Merge branch 'MDL-38509-tool-installaddon' of git://github.com/mudrd8mz/moodle

This commit is contained in:
Dan Poltawski 2013-04-03 11:28:49 +08:00
commit 6833c6d901
47 changed files with 3318 additions and 15 deletions

View File

@ -59,10 +59,18 @@ $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);
// Check some PHP server settings
$PAGE->set_url('/admin/index.php');
if (is_null($newaddonreq)) {
$PAGE->set_url('/admin/index.php');
} else {
// We need to set the eventual add-on installation request in the $PAGE's URL
// so that it is stored in $SESSION->wantsurl and the admin is redirected
// correctly once they are logged-in.
$PAGE->set_url('/admin/index.php', array('installaddonrequest' => $newaddonreq));
}
$PAGE->set_pagelayout('admin'); // Set a default pagelayout
$documentationlink = '<a href="http://docs.moodle.org/en/Installation">Installation docs</a>';
@ -423,6 +431,17 @@ if (!empty($id) and $id == $CFG->siteidentifier) {
set_config('registered', time());
}
// Check if we are returning from an add-on installation request at moodle.org/plugins
if (!is_null($newaddonreq)) {
if (!empty($CFG->disableonclickaddoninstall)) {
// The feature is disabled in config.php, ignore the request.
} else {
redirect(new moodle_url('/admin/tool/installaddon/index.php', array(
'installaddonrequest' => $newaddonreq,
'confirm' => 0)));
}
}
// setup critical warnings before printing admin tree block
$insecuredataroot = is_dataroot_insecure(true);
$SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR);

View File

@ -0,0 +1,574 @@
<?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_installer 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 main plugin features.
*
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_installaddon_installer {
/** @var tool_installaddon_installfromzip */
protected $installfromzipform = null;
/**
* Factory method returning an instance of this class.
*
* @return tool_installaddon_installer
*/
public static function instance() {
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
*
* @return moodle_url
*/
public function get_addons_repository_url() {
global $CFG;
if (!empty($CFG->config_php_settings['alternativeaddonsrepositoryurl'])) {
$url = $CFG->config_php_settings['alternativeaddonsrepositoryurl'];
} else {
$url = 'https://moodle.org/plugins/get.php';
}
if (!$this->should_send_site_info()) {
return new moodle_url($url);
}
// Append the basic information about our site.
$site = array(
'fullname' => $this->get_site_fullname(),
'url' => $this->get_site_url(),
'majorversion' => $this->get_site_major_version(),
);
$site = $this->encode_site_information($site);
return new moodle_url($url, array('site' => $site));
}
/**
* @return tool_installaddon_installfromzip
*/
public function get_installfromzip_form() {
global $CFG;
require_once(dirname(__FILE__).'/installfromzip_form.php');
if (!is_null($this->installfromzipform)) {
return $this->installfromzipform;
}
$action = $this->index_url();
$customdata = array('installer' => $this);
$this->installfromzipform = new tool_installaddon_installfromzip($action, $customdata);
return $this->installfromzipform;
}
/**
* Saves the ZIP file from the {@link tool_installaddon_installfromzip} form
*
* The file is saved into the given temporary location for inspection and eventual
* deployment. The form is expected to be submitted and validated.
*
* @param tool_installaddon_installfromzip $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
*/
public function save_installfromzip_file(tool_installaddon_installfromzip $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) {
if (!empty($rootdir)) {
$files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
}
return $files;
} else {
return array();
}
}
/**
* Returns localised list of available plugin types
*
* @return array (string)plugintype => (string)plugin name
*/
public function get_plugin_types_menu() {
global $CFG;
require_once($CFG->libdir.'/pluginlib.php');
$pluginman = plugin_manager::instance();
$menu = array('' => get_string('choosedots'));
foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) {
$menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')';
}
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 (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
*
* 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;
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,
);
$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.
$options['cainfo'] = $cacertfile;
}
$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']));
}
}
//// End of external API ///////////////////////////////////////////////////
/**
* @see self::instance()
*/
protected function __construct() {
}
/**
* @return string this site full name
*/
protected function get_site_fullname() {
global $SITE;
return strip_tags($SITE->fullname);
}
/**
* @return string this site URL
*/
protected function get_site_url() {
global $CFG;
return $CFG->wwwroot;
}
/**
* @return string major version like 2.5, 2.6 etc.
*/
protected function get_site_major_version() {
return moodle_major_version();
}
/**
* Encodes the given array in a way that can be safely appended as HTTP GET param
*
* Be ware! The recipient may rely on the exact way how the site information is encoded.
* Do not change anything here unless you know what you are doing and understand all
* consequences! (Don't you love warnings like that, too? :-p)
*
* @param array $info
* @return string
*/
protected function encode_site_information(array $info) {
return base64_encode(json_encode($info));
}
/**
* Decide if the encoded site information should be sent to the add-ons repository site
*
* For now, we just return true. In the future, we may want to implement some
* privacy aware logic (based on site/user preferences for example).
*
* @return bool
*/
protected function should_send_site_info() {
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
*
* @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));
}
}

View File

@ -0,0 +1,95 @@
<?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/>.
/**
* @package tool_installaddon
* @subpackage classes
* @category form
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
/**
* Defines a simple form for uploading the add-on ZIP package
*
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_installaddon_installfromzip extends moodleform {
/**
* Defines the form elements
*/
public function definition() {
$mform = $this->_form;
$installer = $this->_customdata['installer'];
$mform->addElement('header', 'general', get_string('installfromzip', 'tool_installaddon'));
$mform->addHelpButton('general', 'installfromzip', 'tool_installaddon');
$options = $installer->get_plugin_types_menu();
$mform->addElement('select', 'plugintype', get_string('installfromziptype', 'tool_installaddon'), $options,
array('id' => 'tool_installaddon_installfromzip_plugintype'));
$mform->addHelpButton('plugintype', 'installfromziptype', 'tool_installaddon');
$mform->addRule('plugintype', null, 'required', null, 'client');
$mform->addElement('static', 'permcheck', '',
html_writer::span(get_string('permcheck', 'tool_installaddon'), '',
array('id' => 'tool_installaddon_installfromzip_permcheck')));
$mform->addElement('filepicker', 'zipfile', get_string('installfromzipfile', 'tool_installaddon'),
null, array('accepted_types' => '.zip'));
$mform->addHelpButton('zipfile', 'installfromzipfile', 'tool_installaddon');
$mform->addRule('zipfile', null, 'required', null, 'client');
$mform->addElement('text', 'rootdir', get_string('installfromziprootdir', 'tool_installaddon'));
$mform->addHelpButton('rootdir', 'installfromziprootdir', 'tool_installaddon');
$mform->setType('rootdir', PARAM_PLUGIN);
$mform->setAdvanced('rootdir');
$mform->addElement('checkbox', 'acknowledgement', get_string('acknowledgement', 'tool_installaddon'),
' '.get_string('acknowledgementtext', 'tool_installaddon'));
$mform->addRule('acknowledgement', get_string('acknowledgementmust', 'tool_installaddon'), 'required', null, 'client');
$this->add_action_buttons(false, get_string('installfromzipsubmit', 'tool_installaddon'));
}
/**
* Validate the form fields
*
* @param array $data
* @param array $files
* @return array (string)field name => (string)validation error text
*/
public function validation($data, $files) {
$installer = $this->_customdata['installer'];
$errors = parent::validation($data, $files);
if (!$installer->is_plugintype_writable($data['plugintype'])) {
$path = $installer->get_plugintype_root($data['plugintype']);
$errors['plugintype'] = get_string('permcheckresultno', 'tool_installaddon', array('path' => $path));
}
return $errors;
}
}

View File

@ -0,0 +1,216 @@
<?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,
);
$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));
}
}

View File

@ -0,0 +1,575 @@
<?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 validation class to check the plugin ZIP contents
*
* 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>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if (!defined('T_ML_COMMENT')) {
define('T_ML_COMMENT', T_COMMENT);
} else {
define('T_DOC_COMMENT', T_ML_COMMENT);
}
/**
* Validates the contents of extracted plugin ZIP file
*
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_installaddon_validator {
/** Critical error message level, causes the validation fail. */
const ERROR = 'error';
/** Warning message level, validation does not fail but the admin should be always informed. */
const WARNING = 'warning';
/** Information message level that the admin should be aware of. */
const INFO = 'info';
/** Debugging message level, should be displayed in debugging mode only. */
const DEBUG = 'debug';
/** @var string full path to the extracted ZIP contents */
protected $extractdir = null;
/** @var array as returned by {@link zip_packer::extract_to_pathname()} */
protected $extractfiles = null;
/** @var bool overall result of validation */
protected $result = null;
/** @var string the name of the plugin root directory */
protected $rootdir = null;
/** @var array explicit list of expected/required characteristics of the ZIP */
protected $assertions = null;
/** @var array of validation log messages */
protected $messages = array();
/** @var array|null array of relevant data obtained from version.php */
protected $versionphp = null;
/** @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
*/
public static function instance($zipcontentpath, array $zipcontentfiles) {
return new static($zipcontentpath, $zipcontentfiles);
}
/**
* Set the expected plugin type, fail the validation otherwise
*
* @param string $required plugin type
*/
public function assert_plugin_type($required) {
$this->assertions['plugintype'] = $required;
}
/**
* Set the expectation that the plugin can be installed into the given Moodle version
*
* @param string $required Moodle version we are about to install to
*/
public function assert_moodle_version($required) {
$this->assertions['moodleversion'] = $required;
}
/**
* Execute the validation process against all explicit and implicit requirements
*
* Returns true if the validation passes (all explicit and implicit requirements
* pass) and the plugin can be installed. Returns false if the validation fails
* (some explicit or implicit requirement fails) and the plugin must not be
* installed.
*
* @return bool
*/
public function execute() {
$this->result = (
$this->validate_files_layout()
and $this->validate_version_php()
and $this->validate_language_pack()
and $this->validate_target_location()
);
return $this->result;
}
/**
* Returns overall result of the validation.
*
* Null is returned if the validation has not been executed yet. Otherwise
* this method returns true (the installation can continue) or false (it is not
* safe to continue with the installation).
*
* @return bool|null
*/
public function get_result() {
return $this->result;
}
/**
* Return the list of validation log messages
*
* Each validation message is a plain object with properties level, msgcode
* and addinfo.
*
* @return array of (int)index => (stdClass) validation message
*/
public function get_messages() {
return $this->messages;
}
/**
* 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).
*
* @return null|array
*/
public function get_versionphp_info() {
return $this->versionphp;
}
/**
* Returns the name of the English language file without the .php extension
*
* This can be used as a suggestion for fixing the plugin root directory in the
* ZIP file during the upload. If no file was found, or multiple PHP files are
* located in lang/en/ folder, then null is returned.
*
* @return null|string
*/
public function get_language_file_name() {
return $this->langfilename;
}
/**
* Returns the rootdir of the extracted package (after eventual renaming)
*
* @return string|null
*/
public function get_rootdir() {
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;
}
/**
* Get the URL to continue to after successful validation
*
* Null is returned if the URL has not been explicitly set by the caller.
*
* @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
*/
protected function __construct($zipcontentpath, array $zipcontentfiles) {
$this->extractdir = $zipcontentpath;
$this->extractfiles = $zipcontentfiles;
}
// Validation methods //////////////////////////////////////////////////////
/**
* @return bool false if files in the ZIP do not have required layout
*/
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
$this->add_message(self::ERROR, 'filesnumber');
return false;
}
foreach ($this->extractfiles as $filerelname => $filestatus) {
if ($filestatus !== true) {
$this->add_message(self::ERROR, 'filestatus', array('file' => $filerelname, 'status' => $filestatus));
return false;
}
}
foreach (array_keys($this->extractfiles) as $filerelname) {
if (!file_exists($this->extractdir.'/'.$filerelname)) {
$this->add_message(self::ERROR, 'filenotexists', array('file' => $filerelname));
return false;
}
}
foreach (array_keys($this->extractfiles) as $filerelname) {
$matches = array();
if (!preg_match("#^([^/]+)/#", $filerelname, $matches) or (!is_null($this->rootdir) and $this->rootdir !== $matches[1])) {
$this->add_message(self::ERROR, 'onedir');
return false;
}
$this->rootdir = $matches[1];
}
if ($this->rootdir !== clean_param($this->rootdir, PARAM_PLUGIN)) {
$this->add_message(self::ERROR, 'rootdirinvalid', $this->rootdir);
return false;
} else {
$this->add_message(self::INFO, 'rootdir', $this->rootdir);
}
return is_dir($this->extractdir.'/'.$this->rootdir);
}
/**
* @return bool false if the version.php file does not declare required information
*/
protected function validate_version_php() {
if (!isset($this->assertions['plugintype'])) {
throw new coding_exception('Required plugin type must be set before calling this');
}
if (!isset($this->assertions['moodleversion'])) {
throw new coding_exception('Required Moodle version must be set before calling this');
}
$fullpath = $this->extractdir.'/'.$this->rootdir.'/version.php';
if (!file_exists($fullpath)) {
// This is tolerated for themes only.
if ($this->assertions['plugintype'] === 'theme') {
$this->add_message(self::DEBUG, 'missingversionphp');
return true;
} else {
$this->add_message(self::ERROR, 'missingversionphp');
return false;
}
}
$this->versionphp = array();
$info = $this->parse_version_php($fullpath);
if ($this->assertions['plugintype'] === 'mod') {
$type = 'module';
} else {
$type = 'plugin';
}
if (!isset($info[$type.'->version'])) {
if ($type === 'module' and isset($info['plugin->version'])) {
// Expect the activity module using $plugin in version.php instead of $module.
$type = 'plugin';
$this->versionphp['version'] = $info[$type.'->version'];
$this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']);
} else {
$this->add_message(self::ERROR, 'missingversion');
return false;
}
} else {
$this->versionphp['version'] = $info[$type.'->version'];
$this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']);
}
if (isset($info[$type.'->requires'])) {
$this->versionphp['requires'] = $info[$type.'->requires'];
if ($this->versionphp['requires'] > $this->assertions['moodleversion']) {
$this->add_message(self::ERROR, 'requiresmoodle', $this->versionphp['requires']);
return false;
}
$this->add_message(self::INFO, 'requiresmoodle', $this->versionphp['requires']);
}
if (isset($info[$type.'->component'])) {
$this->versionphp['component'] = $info[$type.'->component'];
list($reqtype, $reqname) = normalize_component($this->versionphp['component']);
if ($reqtype !== $this->assertions['plugintype']) {
$this->add_message(self::ERROR, 'componentmismatchtype', array(
'expected' => $this->assertions['plugintype'],
'found' => $reqtype));
return false;
}
if ($reqname !== $this->rootdir) {
$this->add_message(self::ERROR, 'componentmismatchname', $reqname);
return false;
}
$this->add_message(self::INFO, 'componentmatch', $this->versionphp['component']);
}
if (isset($info[$type.'->maturity'])) {
$this->versionphp['maturity'] = $info[$type.'->maturity'];
if ($this->versionphp['maturity'] === 'MATURITY_STABLE') {
$this->add_message(self::INFO, 'maturity', $this->versionphp['maturity']);
} else {
$this->add_message(self::WARNING, 'maturity', $this->versionphp['maturity']);
}
}
if (isset($info[$type.'->release'])) {
$this->versionphp['release'] = $info[$type.'->release'];
$this->add_message(self::INFO, 'release', $this->versionphp['release']);
}
return true;
}
/**
* @return bool false if the English language pack is not provided correctly
*/
protected function validate_language_pack() {
if (!isset($this->assertions['plugintype'])) {
throw new coding_exception('Required plugin type must be set before calling this');
}
if (!isset($this->extractfiles[$this->rootdir.'/lang/en/'])
or $this->extractfiles[$this->rootdir.'/lang/en/'] !== true
or !is_dir($this->extractdir.'/'.$this->rootdir.'/lang/en')) {
$this->add_message(self::ERROR, 'missinglangenfolder');
return false;
}
$langfiles = array();
foreach (array_keys($this->extractfiles) as $extractfile) {
$matches = array();
if (preg_match('#^'.preg_quote($this->rootdir).'/lang/en/([^/]+).php?$#i', $extractfile, $matches)) {
$langfiles[] = $matches[1];
}
}
if (empty($langfiles)) {
$this->add_message(self::ERROR, 'missinglangenfile');
return false;
} else if (count($langfiles) > 1) {
$this->add_message(self::WARNING, 'multiplelangenfiles');
} else {
$this->langfilename = $langfiles[0];
$this->add_message(self::DEBUG, 'foundlangfile', $this->langfilename);
}
if ($this->assertions['plugintype'] === 'mod') {
$expected = $this->rootdir.'.php';
} else {
$expected = $this->assertions['plugintype'].'_'.$this->rootdir.'.php';
}
if (!isset($this->extractfiles[$this->rootdir.'/lang/en/'.$expected])
or $this->extractfiles[$this->rootdir.'/lang/en/'.$expected] !== true
or !is_file($this->extractdir.'/'.$this->rootdir.'/lang/en/'.$expected)) {
$this->add_message(self::ERROR, 'missingexpectedlangenfile', $expected);
return false;
}
return true;
}
/**
* @return bool false of the given add-on can't be installed into its location
*/
public function validate_target_location() {
if (!isset($this->assertions['plugintype'])) {
throw new coding_exception('Required plugin type must be set before calling this');
}
$plugintypepath = $this->get_plugintype_location($this->assertions['plugintype']);
if (is_null($plugintypepath)) {
$this->add_message(self::ERROR, 'unknowntype', $this->assertions['plugintype']);
return false;
}
if (!is_dir($plugintypepath)) {
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);
return false;
}
if (is_writable($plugintypepath)) {
$this->add_message(self::INFO, 'pathwritable', $plugintypepath);
} else {
$this->add_message(self::ERROR, 'pathwritable', $plugintypepath);
return false;
}
return true;
}
// Helper methods //////////////////////////////////////////////////////////
/**
* Get as much information from existing version.php as possible
*
* @param string full path to the version.php file
* @return array of found meta-info declarations
*/
protected function parse_version_php($fullpath) {
$content = $this->get_stripped_file_contents($fullpath);
preg_match_all('#\$((plugin|module)\->(version|maturity|release|requires))=()(\d+(\.\d+)?);#m', $content, $matches1);
preg_match_all('#\$((plugin|module)\->(maturity))=()(MATURITY_\w+);#m', $content, $matches2);
preg_match_all('#\$((plugin|module)\->(release))=([\'"])(.*?)\4;#m', $content, $matches3);
preg_match_all('#\$((plugin|module)\->(component))=([\'"])(.+?_.+?)\4;#m', $content, $matches4);
if (count($matches1[1]) + count($matches2[1]) + count($matches3[1]) + count($matches4[1])) {
$info = array_combine(
array_merge($matches1[1], $matches2[1], $matches3[1], $matches4[1]),
array_merge($matches1[5], $matches2[5], $matches3[5], $matches4[5])
);
} else {
$info = array();
}
return $info;
}
/**
* Append the given message to the messages log
*
* @param string $level e.g. self::ERROR
* @param string $msgcode may form a string
* @param string|array|object $a optional additional info suitable for {@link get_string()}
*/
protected function add_message($level, $msgcode, $a = null) {
$msg = (object)array(
'level' => $level,
'msgcode' => $msgcode,
'addinfo' => $a,
);
$this->messages[] = $msg;
}
/**
* Returns bare PHP code from the given file
*
* Returns contents without PHP opening and closing tags, text outside php code,
* comments and extra whitespaces.
*
* @param string $fullpath full path to the file
* @return string
*/
protected function get_stripped_file_contents($fullpath) {
$source = file_get_contents($fullpath);
$tokens = token_get_all($source);
$output = '';
$doprocess = false;
foreach ($tokens as $token) {
if (is_string($token)) {
// Simple one character token.
$id = -1;
$text = $token;
} else {
// Token array.
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 .= ' ';
}
}
break;
}
}
return $output;
}
/**
* Returns the full path to the root directory of the given plugin type
*
* @param string $plugintype
* @return string|null
*/
public function get_plugintype_location($plugintype) {
$plugintypepath = null;
foreach (get_plugin_types() as $type => $fullpath) {
if ($type === $plugintype) {
$plugintypepath = $fullpath;
break;
}
}
return $plugintypepath;
}
}

View File

@ -0,0 +1,76 @@
<?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_once(dirname(__FILE__).'/classes/installer.php');
require_once(dirname(__FILE__).'/classes/validator.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'));
}
rename($zipcontentpath.'/'.$pluginname, $plugintypepath.'/'.$pluginname);
fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid);
redirect(new moodle_url('/admin'));

View File

@ -0,0 +1,67 @@
<?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 main screen of the tool.
*
* @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(dirname(__FILE__).'/classes/installer.php');
admin_externalpage_setup('tool_installaddon_index');
if (!empty($CFG->disableonclickaddoninstall)) {
notice(get_string('featuredisabled', 'tool_installaddon'));
}
$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()) {
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);
// Redirect to the validation page.
$nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
'sesskey' => sesskey(),
'jobid' => $jobid,
'zip' => $zipfilename,
'type' => $data->plugintype,
'rootdir' => $data->rootdir));
redirect($nexturl);
}
// Output starts here.
echo $output->index_page();

View File

@ -0,0 +1,102 @@
<?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/>.
/**
* Strings for the tool_installaddon component.
*
* @package tool_installaddon
* @category string
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['acknowledgement'] = 'Acknowledgement';
$string['acknowledgementmust'] = 'You must acknowledge this';
$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing add-ons. I accept and understand that add-ons (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
$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';
$string['installfromzip_help'] = 'Alternatively to installing add-ons directly from the Moodle plugins directory, you can install add-ons from manually uploaded ZIP packages. Such ZIP packages are expected to have same structure as the ones available in the Moodle plugins directory.';
$string['installfromzipfile'] = 'ZIP package';
$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory with the name of the plugin. The ZIP will be extracted into the appropriate location for the given plugin type. Packages downloaded from the Moodle plugins directory have this format.';
$string['installfromziprootdir'] = 'Rename the root directory';
$string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You can rename the root directory of the extracted package to the correct value defined in this field.';
$string['installfromzipsubmit'] = 'Install add-on from the ZIP file';
$string['installfromziptype'] = 'Plugin type';
$string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. The installation procedure may fail badly when incorrect plugin type is provided.';
$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process';
$string['permcheckerror'] = 'Error while checking for write permission';
$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';
$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the declared add-on name.';
$string['validationmsg_componentmismatchname_info'] = 'The add-on declares its name is \'{$a}\' but that does not match the name of the root directory.';
$string['validationmsg_componentmismatchtype'] = 'Add-on type mismatch';
$string['validationmsg_componentmismatchtype_info'] = 'You have selected the type \'{$a->expected}\' but the add-on 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_maturity'] = 'Declared maturity level';
$string['validationmsg_maturity_help'] = 'The add-on can declare its maturity level. If the maintainer considers the add-on 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_missingexpectedlangenfile'] = 'English language file name mismatch';
$string['validationmsg_missingexpectedlangenfile_info'] = 'The given add-on type would need to provide the English language file {$a}.';
$string['validationmsg_missinglangenfile'] = 'No English language file found';
$string['validationmsg_missinglangenfolder'] = 'Missing English language folder';
$string['validationmsg_missingversion'] = 'Add-on 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 add-on code. The name of that root directory must match the name of the plugin.';
$string['validationmsg_pathwritable'] = 'Write access check';
$string['validationmsg_pluginversion'] = 'Add-on version';
$string['validationmsg_release'] = 'Add-on release';
$string['validationmsg_requiresmoodle'] = 'Required Moodle version';
$string['validationmsg_rootdir'] = 'Name of the add-on to be installed';
$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the add-on to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the add-on.';
$string['validationmsg_rootdirinvalid'] = 'Invalid name of the add-on';
$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 incorrect name of the root directory. You have to fix the name of the root directory to match the add-on name.';
$string['validationmsg_targetexists'] = 'Target location already exists';
$string['validationmsg_targetexists_help'] = 'The directory that the add-on is to be installed to, must not exist yet.';
$string['validationmsg_unknowntype'] = 'Unknown plugin type';
$string['validationmsglevel_debug'] = 'Debug';
$string['validationmsglevel_error'] = 'Error';
$string['validationmsglevel_info'] = 'OK';
$string['validationmsglevel_warning'] = 'Warning';
$string['validationresult0'] = 'Validation failed!';
$string['validationresult0_help'] = 'Some serious problem was detected. It is not safe to install the add-on. See the validation log messages for more details.';
$string['validationresult1'] = 'Validation passed!';
$string['validationresult1_help'] = 'No serious problems were detected. You can continue with the add-on installation. See the validation log messages for more details and eventual warnings.';
$string['validationresult1_help'] = 'The add-on package has been validated and no serious problems were detected.';
$string['validationresultinfo'] = 'Info';
$string['validationresultmsg'] = 'Message';
$string['validationresultstatus'] = 'Status';

View File

@ -0,0 +1,74 @@
<?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/>.
/**
* Checks the write permission for the given plugin type
*
* @package tool_installaddon
* @subpackage ajax
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('AJAX_SCRIPT', true);
require(dirname(__FILE__) . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once(dirname(__FILE__).'/classes/installer.php');
require_login();
if (!has_capability('moodle/site:config', context_system::instance())) {
header('HTTP/1.1 403 Forbidden');
die();
}
if (!empty($CFG->disableonclickaddoninstall)) {
header('HTTP/1.1 403 Forbidden');
die();
}
if (!confirm_sesskey()) {
header('HTTP/1.1 403 Forbidden');
die();
}
$plugintype = optional_param('plugintype', null, PARAM_ALPHANUMEXT);
if (is_null($plugintype)) {
header('HTTP/1.1 400 Bad Request');
die();
}
$installer = tool_installaddon_installer::instance();
$plugintypepath = $installer->get_plugintype_root($plugintype);
if (empty($plugintypepath)) {
header('HTTP/1.1 400 Bad Request');
die();
}
$response = array('path' => $plugintypepath);
if ($installer->is_plugintype_writable($plugintype)) {
$response['writable'] = 1;
} else {
$response['writable'] = 0;
}
header('Content-Type: application/json; charset: utf-8');
echo json_encode($response);

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
<defs>
</defs>
<path style="fill:#999999;" d="M16,9v6c0,0.5-0.5,1-1,1h-1H2H1c-0.5,0-1-0.5-1-1V9c0-0.5,0.5-1,1-1h1c0.5,0,1,0.5,1,1v4h10V9
c0-0.5,0.5-1,1-1h1C15.5,8,16,8.5,16,9z M12.4,5.1l-0.7-0.7c-0.4-0.4-1-0.4-1.4,0L9.5,5.2V1c0-0.5-0.4-1-1-1h-1c-0.5,0-1,0.5-1,1
v4.2L5.7,4.4C5.3,4,4.7,4,4.3,4.4L3.6,5.1c-0.4,0.4-0.4,1,0,1.4l3.7,3.7c0.2,0.2,0.5,0.3,0.7,0.3c0.3,0,0.5-0.1,0.7-0.3l3.7-3.7
C12.8,6.2,12.8,5.5,12.4,5.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,398 @@
<?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/>.
/**
* Output rendering for the plugin.
*
* @package tool_installaddon
* @category output
* @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 the plugin renderer
*
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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.
*
* @throws coding_exception if the installer has been already set
* @param tool_installaddon_installer $installer
*/
public function set_installer_instance(tool_installaddon_installer $installer) {
if (is_null($this->installer)) {
$this->installer = $installer;
} else {
throw new coding_exception('Attempting to reset the installer instance.');
}
}
/**
* 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
*
* @return string
*/
public function index_page() {
if (is_null($this->installer)) {
throw new coding_exception('Installer instance has not been set.');
}
$permcheckurl = new moodle_url('/admin/tool/installaddon/permcheck.php');
$this->page->requires->yui_module('moodle-tool_installaddon-permcheck', 'M.tool_installaddon.permcheck.init',
array(array('permcheckurl' => $permcheckurl->out())));
$this->page->requires->strings_for_js(
array('permcheckprogress', 'permcheckresultno', 'permcheckresultyes', 'permcheckerror'), 'tool_installaddon');
$out = $this->output->header();
$out .= $this->index_page_heading();
$out .= $this->index_page_repository();
$out .= $this->index_page_upload();
$out .= $this->output->footer();
return $out;
}
/**
* Defines the validation results page layout
*
* @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.');
}
$out = $this->output->header();
$out .= $this->validation_page_heading();
$out .= $this->validation_page_messages();
$out .= $this->validation_page_continue();
$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 /////////////////////////////////////////////////
/**
* Renders the index page heading
*
* @return string
*/
protected function index_page_heading() {
return $this->output->heading(get_string('pluginname', 'tool_installaddon'));
}
/**
* Renders the widget for browsing the add-on repository
*
* @return string
*/
protected function index_page_repository() {
$url = $this->installer->get_addons_repository_url();
$out = $this->box(
$this->output->single_button($url, get_string('installfromrepo', 'tool_installaddon'), 'get').
$this->output->help_icon('installfromrepo', 'tool_installaddon'),
'generalbox', 'installfromrepobox'
);
return $out;
}
/**
* Renders the widget for uploading the add-on ZIP package
*
* @return string
*/
protected function index_page_upload() {
$form = $this->installer->get_installfromzip_form();
ob_start();
$form->display();
$out = ob_get_clean();
$out = $this->box($out, 'generalbox', 'installfromzipbox');
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() {
$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'));
}
$cancelbutton = $this->output->single_button(
new moodle_url('/admin/tool/installaddon/index.php'), get_string('cancel', 'core'), 'get',
array('class' => 'singlebutton cancelbutton'));
return $this->output->container($cancelbutton.$contbutton, 'postvalidationbuttons');
}
}

View File

@ -0,0 +1,39 @@
<?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/>.
/**
* Puts the plugin actions into the admin settings tree.
*
* @package tool_installaddon
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig and empty($CFG->disableonclickaddoninstall)) {
$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');
}

View File

@ -0,0 +1,68 @@
#page-admin-tool-installaddon-index #installfromrepobox {
text-align: center;
padding-top: 2em;
padding-bottom: 2em;
}
#page-admin-tool-installaddon-index #installfromrepobox .singlebutton {
display: inline-block;
}
#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;
}

View File

@ -0,0 +1 @@
Plugin must have more than one file.

View File

@ -0,0 +1,4 @@
<?php
$plugin->component = 'repository_mahara';
$plugin->version = 2014010100;

View File

@ -0,0 +1,3 @@
<?php
echo 'One, my little hobbit, never installs malicisous add-ons';

View File

@ -0,0 +1,3 @@
<?php
$string['pluginname'] = 'Foo bar!';

View File

@ -0,0 +1,4 @@
<?php
$plugin->version = 2013031900;
$plugin->component = 'local_greenbar';

View File

@ -0,0 +1,3 @@
<?php
$plugin->component = 'local_one';

View File

@ -0,0 +1 @@
Only one dir is allowed

View File

@ -0,0 +1 @@
<?php

View File

@ -0,0 +1 @@
<?php

View File

@ -0,0 +1,3 @@
<?php
$string['pluginnname'] = 'Root directory mismatch';

View File

@ -0,0 +1,3 @@
<?php

View File

@ -0,0 +1,4 @@
<?php
$module->version = 2014122455;
$plugin->version = 2014122455;

View File

@ -0,0 +1,3 @@
<?php
echo 'Do not use hardcoded strings, provide the language pack';

View File

@ -0,0 +1,3 @@
<?php
$string['pluginversion'] = 'Activity module with no version.php';

View File

@ -0,0 +1,3 @@
<?php
$string['pluginversion'] = 'A theme with no version.php';

View File

@ -0,0 +1,3 @@
<?php
// index.php

View File

@ -0,0 +1,3 @@
<?php
$string['pluginname'] = 'Foo!';

View File

@ -0,0 +1,3 @@
<?php // $Id$
// I don't miss CVS, do you?

View File

@ -0,0 +1,3 @@
<?php
echo 'One, my little hobbit, never installs malicisous add-ons';

View File

@ -0,0 +1,3 @@
<?php
$string['pluginname'] = 'Foo bar!';

View File

@ -0,0 +1,9 @@
<?php
$module->version = 10; // Ignored, this should use $plugin
$plugin->version = 2013031900;
$plugin->component = 'local_foobar';
$plugin->requires = 2013031200;
$module->release = 'We are not an activity module!';
$plugin->maturity = MATURITY_ALPHA;
//$plugin->release = 'And this is commented';

View File

@ -0,0 +1,52 @@
<h1>Example version.php file</h1>
<p>version.php is required for all plugins but themes.</p>
<h2>Example of values</h2>
<pre>
$plugin->version = 2011051000;
$plugin->requires = 2010112400;
$plugin->cron = 0;
$plugin->component = 'plugintype_pluginname';
$plugin->maturity = MATURITY_STABLE;
$plugin->release = '2.x (Build: 2011051000)';
$plugin->dependencies = array('mod_forum' => ANY_VERSION, 'mod_data' => 2010020300);
</pre>
Replace $plugin with $module for activity modules, as in
<pre>
$module->version = 2012122400;
</pre><?php // $Id$ $module->version = 1;
$plugin->component
= 'old_foobar';//$plugin->component='commented';
$plugin->component =
'block_foobar';
$plugin->version = 2013010100;
////////$plugin->version = 0;
/* for activity
modules use:
$module->version = 2014131300;
***/
$plugin->version = "2010091855"; // Do not use quotes here.
$plugin->version = '2010091856.9'; // Do not use quotes here.
$plugin->requires = /* 2012010100 */ 2012122401 ;
$module->maturity = MATURITY_STABLE;
$module->maturity = 50; // If both present, the constant wins (on contrary to what PHP would do)
$module->maturity = 'MATURITY_BETA'; // Do not use quotes here.
$plugin->maturity = 10;
$plugin->maturity = MATURITY_ALPHA;
$module->release = 2.3; $plugin->release = 'v2.4';
$module->release = "v2.3"; $plugin->release = 2.4;

Binary file not shown.

View File

@ -0,0 +1,158 @@
<?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 the unit tests class and some helper classes
*
* @package tool_installaddon
* @category test
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/installaddon/classes/installer.php');
/**
* Unit tests for the {@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_test extends advanced_testcase {
public function test_get_addons_repository_url() {
$installer = testable_tool_installaddon_installer::instance();
$url = $installer->get_addons_repository_url();
$query = parse_url($url, PHP_URL_QUERY);
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
$site = rawurldecode($matches[1]);
$site = json_decode(base64_decode($site), true);
$this->assertEquals('array', gettype($site));
$this->assertEquals(3, count($site));
$this->assertSame('Nasty site', $site['fullname']);
$this->assertSame('file:///etc/passwd', $site['url']);
$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->assertEquals('array', gettype($files));
$this->assertEquals(4, count($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();
$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));
}
}
/**
* 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;
}
}

View File

@ -0,0 +1,335 @@
<?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 the unit tests class and some helper classes
*
* @package tool_installaddon
* @category test
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/installaddon/classes/validator.php');
/**
* Unit tests for the {@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_validator_test extends basic_testcase {
public function test_validate_files_layout() {
$fixtures = dirname(__FILE__).'/fixtures';
// Non-existing directory.
$validator = testable_tool_installaddon_validator::instance($fixtures.'/nulldir', array(
'null/' => true,
'null/lang/' => true,
'null/lang/en/' => true,
'null/lang/en/null.php' => true));
$this->assertEquals('testable_tool_installaddon_validator', get_class($validator));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR,
'filenotexists', array('file' => 'null/')));
// Missing expected file
$validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
'foobar/' => true,
'foobar/version.php' => true,
'foobar/index.php' => true,
'foobar/lang/' => true,
'foobar/lang/en/' => true,
'foobar/lang/en/local_foobar.php' => true,
'foobar/NOTEXISTS.txt' => true));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR,
'filenotexists', array('file' => 'foobar/NOTEXISTS.txt')));
// Errors during ZIP extraction
$validator = testable_tool_installaddon_validator::instance($fixtures.'/multidir', array(
'one/' => true,
'one/version.php' => 'Can not write target file',
'two/' => true,
'two/README.txt' => true));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'filestatus',
array('file' => 'one/version.php', 'status' => 'Can not write target file')));
// Insufficient number of extracted files
$validator = testable_tool_installaddon_validator::instance($fixtures.'/emptydir', array(
'emptydir/' => true,
'emptydir/README.txt' => true));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'filesnumber'));
// No wrapping directory
$validator = testable_tool_installaddon_validator::instance($fixtures.'/nowrapdir', array(
'version.php' => true,
'index.php' => true,
'lang/' => true,
'lang/en/' => true,
'lang/en/foo.php' => true));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir'));
// Multiple directories
$validator = testable_tool_installaddon_validator::instance($fixtures.'/multidir', array(
'one/' => true,
'one/version.php' => true,
'two/' => true,
'two/README.txt' => true));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir'));
// Invalid root directory name
$validator = testable_tool_installaddon_validator::instance($fixtures.'/github', array(
'moodle-repository_mahara-master/' => true,
'moodle-repository_mahara-master/lang/' => true,
'moodle-repository_mahara-master/lang/en/' => true,
'moodle-repository_mahara-master/lang/en/repository_mahara.php' => true,
'moodle-repository_mahara-master/version.php' => true));
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'rootdirinvalid',
'moodle-repository_mahara-master'));
}
public function test_validate_version_php() {
$fixtures = dirname(__FILE__).'/fixtures';
$validator = testable_tool_installaddon_validator::instance($fixtures.'/noversiontheme', array(
'noversion/' => true,
'noversion/lang/' => true,
'noversion/lang/en/' => true,
'noversion/lang/en/theme_noversion.php' => true));
$validator->assert_plugin_type('theme');
$validator->assert_moodle_version(0);
$this->assertTrue($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'missingversionphp'));
$this->assertTrue(is_null($validator->get_versionphp_info()));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/noversionmod', array(
'noversion/' => true,
'noversion/lang/' => true,
'noversion/lang/en/' => true,
'noversion/lang/en/noversion.php' => true));
$validator->assert_plugin_type('mod');
$validator->assert_moodle_version(0);
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingversionphp'));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
'foobar/' => true,
'foobar/version.php' => true,
'foobar/index.php' => true,
'foobar/lang/' => true));
$validator->assert_plugin_type('block');
$validator->assert_moodle_version('2013031400.00');
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'componentmismatchtype',
array('expected' => 'block', 'found' => 'local')));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
'foobar/' => true,
'foobar/version.php' => true,
'foobar/index.php' => true,
'foobar/lang/' => true,
'foobar/lang/en/' => true,
'foobar/lang/en/local_foobar.php' => true));
$validator->assert_plugin_type('local');
$validator->assert_moodle_version('2013031400.00');
$this->assertTrue($validator->execute());
$this->assertTrue($validator->get_result());
$this->assertEquals('foobar', $validator->get_rootdir());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'rootdir', 'foobar'));
$versionphpinfo = $validator->get_versionphp_info();
$this->assertEquals('array', gettype($versionphpinfo));
$this->assertEquals(4, count($versionphpinfo));
$this->assertEquals(2013031900, $versionphpinfo['version']);
$this->assertEquals(2013031200, $versionphpinfo['requires']);
$this->assertEquals('local_foobar', $versionphpinfo['component']);
$this->assertEquals('MATURITY_ALPHA', $versionphpinfo['maturity']); // Note we get the constant name here.
$this->assertEquals(MATURITY_ALPHA, constant($versionphpinfo['maturity'])); // This is how to get the real value.
$this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'maturity', 'MATURITY_ALPHA'));
}
public function test_validate_language_pack() {
$fixtures = dirname(__FILE__).'/fixtures';
$validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
'bah/' => true,
'bah/index.php' => true,
'bah/view.php' => true,
'bah/version.php' => true));
$validator->assert_plugin_type('mod');
$validator->assert_moodle_version(0);
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfolder'));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
'bah/' => true,
'bah/version.php' => true,
'bah/lang/' => true,
'bah/lang/en/' => true));
$validator->assert_plugin_type('mod');
$validator->assert_moodle_version(0);
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfile'));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
'bah/' => true,
'bah/version.php' => true,
'bah/lang/' => true,
'bah/lang/en/' => true,
'bah/lang/en/bleh.php' => true,
'bah/lang/en/bah.php' => true));
$validator->assert_plugin_type('mod');
$validator->assert_moodle_version(0);
$this->assertTrue($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'multiplelangenfiles'));
$this->assertTrue(is_null($validator->get_language_file_name()));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array(
'bah/' => true,
'bah/version.php' => true,
'bah/lang/' => true,
'bah/lang/en/' => true,
'bah/lang/en/bah.php' => true));
$validator->assert_plugin_type('block');
$validator->assert_moodle_version(0);
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingexpectedlangenfile', 'block_bah.php'));
$this->assertEquals('bah', $validator->get_language_file_name());
$validator = testable_tool_installaddon_validator::instance($fixtures.'/noversiontheme', array(
'noversion/' => true,
'noversion/lang/' => true,
'noversion/lang/en/' => true,
'noversion/lang/en/theme_noversion.php' => true));
$validator->assert_plugin_type('theme');
$validator->assert_moodle_version(0);
$this->assertTrue($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'foundlangfile', 'theme_noversion'));
$this->assertEquals('theme_noversion', $validator->get_language_file_name());
$validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
'foobar/' => true,
'foobar/version.php' => true,
'foobar/index.php' => true,
'foobar/lang/' => true,
'foobar/lang/en/' => true,
'foobar/lang/en/local_foobar.php' => true));
$validator->assert_plugin_type('local');
$validator->assert_moodle_version('2013031400.00');
$this->assertTrue($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'foundlangfile', 'local_foobar'));
$this->assertEquals('local_foobar', $validator->get_language_file_name());
}
public function test_validate_target_location() {
$fixtures = dirname(__FILE__).'/fixtures';
$validator = testable_tool_installaddon_validator::instance($fixtures.'/installed', array(
'greenbar/' => true,
'greenbar/version.php' => true,
'greenbar/index.php' => true,
'greenbar/lang/' => true,
'greenbar/lang/en/' => true,
'greenbar/lang/en/local_greenbar.php' => true));
$validator->assert_plugin_type('local');
$validator->assert_moodle_version('2013031400.00');
$this->assertFalse($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'targetexists',
$validator->get_plugintype_location('local').'/greenbar'));
$validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array(
'foobar/' => true,
'foobar/version.php' => true,
'foobar/index.php' => true,
'foobar/lang/' => true,
'foobar/lang/en/' => true,
'foobar/lang/en/local_foobar.php' => true));
$validator->assert_plugin_type('local');
$validator->assert_moodle_version('2013031400.00');
$this->assertTrue($validator->execute());
$this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'pathwritable',
$validator->get_plugintype_location('local')));
}
public function test_parse_version_php() {
$fixtures = dirname(__FILE__).'/fixtures/versionphp';
$validator = testable_tool_installaddon_validator::instance($fixtures, array());
$this->assertEquals('testable_tool_installaddon_validator', get_class($validator));
$info = $validator->testable_parse_version_php($fixtures.'/version1.php');
$this->assertEquals('array', gettype($info));
$this->assertEquals(7, count($info));
$this->assertEquals('block_foobar', $info['plugin->component']); // Later in the file.
$this->assertEquals('2013010100', $info['plugin->version']); // Numeric wins over strings.
$this->assertEquals('2012122401', $info['plugin->requires']); // Commented.
$this->assertEquals('MATURITY_STABLE', $info['module->maturity']); // Constant wins regardless the order (non-PHP behaviour).
$this->assertEquals('MATURITY_ALPHA', $info['plugin->maturity']); // Constant wins regardless the order (non-PHP behaviour).
$this->assertEquals('v2.3', $info['module->release']); // String wins over numeric (non-PHP behaviour).
$this->assertEquals('v2.4', $info['plugin->release']); // String wins over numeric (non-PHP behaviour).
}
// Helper methods //////////////////////////////////////////////////////////
protected function has_message(array $messages, $level, $msgcode, $addinfo = null) {
foreach ($messages as $message) {
if ($message->level === $level and $message->msgcode === $msgcode and $message->addinfo === $addinfo) {
return true;
}
}
return false;
}
}
/**
* Provides access to protected methods we want to explicitly test
*
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_tool_installaddon_validator extends tool_installaddon_validator {
public function testable_parse_version_php($fullpath) {
return parent::parse_version_php($fullpath);
}
public function get_plugintype_location($plugintype) {
$testableroot = make_temp_directory('testable_tool_installaddon_validator/plugintypes');
if (!is_dir($testableroot.'/'.$plugintype)) {
make_temp_directory('testable_tool_installaddon_validator/plugintypes/'.$plugintype);
}
if ($plugintype === 'local') {
// We need the following for the test_validate_target_location() method
make_temp_directory('testable_tool_installaddon_validator/plugintypes/local/greenbar');
}
return $testableroot.'/'.$plugintype;
}
}

View File

@ -0,0 +1,80 @@
<?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');
require_once(dirname(__FILE__).'/classes/installer.php');
require_once(dirname(__FILE__).'/classes/validator.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();

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* @package tool_installaddon
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'tool_installaddon';
$plugin->version = 2013031400;
$plugin->requires = 2013031400;
$plugin->maturity = MATURITY_BETA;

View File

@ -0,0 +1,105 @@
/**
* Check for write permission for the selected plugin type
*
* @module moodle-tool_installaddon-permcheck
* @author David Mudrak <david@moodle.com>
*/
YUI.add('moodle-tool_installaddon-permcheck', function(Y) {
M.tool_installaddon = M.tool_installaddon || {};
/**
* @class permcheck
* @static
*/
M.tool_installaddon.permcheck = {
/**
* @method init
* @param {Object} config Configuration passed from the PHP
*/
init : function(config) {
this.config = config;
var plugintypesel = Y.one('#tool_installaddon_installfromzip_plugintype');
if (plugintypesel) {
plugintypesel.on('change', this.check_for_permission, this);
}
},
/**
* @method check_for_permission
* @param {Event} e
*/
check_for_permission : function(e) {
var plugintype = e.currentTarget.get('value');
if (plugintype == '') {
return;
}
Y.log('Selected plugin type: ' + plugintype, 'debug', 'moodle-tool_installaddon-permcheck');
Y.io(this.config.permcheckurl, {
'method' : 'GET',
'data' : {
'sesskey' : M.cfg.sesskey,
'plugintype' : plugintype
},
'arguments' : {
'plugintypeselector' : e.currentTarget,
'showresult' : function(msg, status) {
var resultline = Y.one('#tool_installaddon_installfromzip_permcheck');
if (resultline) {
if (status === 'success') {
resultline.setContent('<span class="success"><img src="' + M.util.image_url('i/tick_green_big') + '" /> ' +
msg + '</span>');
} else if (status === 'progress') {
resultline.setContent('<span class="progress"><img src="' + M.cfg.loadingicon + '" /> ' +
msg + '</span>');
} else {
resultline.setContent('<span class="error"><img src="' + M.util.image_url('i/cross_red_big') + '" /> ' +
msg + '</span>');
}
}
}
},
'on' : {
'start' : function(transid, args) {
args.showresult(M.util.get_string('permcheckprogress', 'tool_installaddon'), 'progress');
},
'success': function(transid, outcome, args) {
var response;
try {
response = Y.JSON.parse(outcome.responseText);
if (response.error) {
Y.log(response.error, 'error', 'moodle-tool_installaddon-permcheck');
args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon', response), 'error');
} else if (response.path && response.writable == 1) {
args.showresult(M.util.get_string('permcheckresultyes', 'tool_installaddon', response), 'success');
} else if (response.path && response.writable == 0) {
args.showresult(M.util.get_string('permcheckresultno', 'tool_installaddon', response), 'error');
} else {
Y.log(response, 'debug', 'moodle-tool_installaddon-permcheck');
args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon', response), 'error');
}
} catch (e) {
Y.log(e, 'error', 'moodle-tool_installaddon-permcheck');
args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon'), 'error');
}
},
'failure': function(transid, outcome, args) {
Y.log(outcome.statusText, 'error', 'moodle-tool_installaddon-permcheck');
args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon'));
}
}
});
},
/**
* @property
* @type {Object}
*/
config : null
};
}, '@VERSION@', {
requires:['node', 'event', 'io-base']
});

View File

@ -438,6 +438,16 @@ $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.
//
// $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;
//
// As of version 2.4 Moodle serves icons as SVG images if the users browser appears
// to support SVG.
// For those wanting to control the serving of SVG images the following setting can

View File

@ -96,6 +96,17 @@ class plugin_manager {
}
}
/**
* Returns the result of {@link get_plugin_types()} ordered for humans
*
* @see self::reorder_plugin_types()
* @param bool $fullpaths false means relative paths from dirroot
* @return array (string)name => (string)location
*/
public function get_plugin_types($fullpaths = true) {
return $this->reorder_plugin_types(get_plugin_types($fullpaths));
}
/**
* Returns a tree of known plugins and information about them
*
@ -119,8 +130,7 @@ class plugin_manager {
}
}
$this->pluginsinfo = array();
$plugintypes = get_plugin_types();
$plugintypes = $this->reorder_plugin_types($plugintypes);
$plugintypes = $this->get_plugin_types();
foreach ($plugintypes as $plugintype => $plugintyperootdir) {
if (in_array($plugintype, array('base', 'general'))) {
throw new coding_exception('Illegal usage of reserved word for plugin type');
@ -219,6 +229,35 @@ class plugin_manager {
return $this->pluginsinfo[$type][$name]->displayname;
}
/**
* Returns a localized name of a plugin typed in singular form
*
* Most plugin types define their names in core_plugin lang file. In case of subplugins,
* we try to ask the parent plugin for the name. In the worst case, we will return
* the value of the passed $type parameter.
*
* @param string $type the type of the plugin, e.g. mod or workshopform
* @return string
*/
public function plugintype_name($type) {
if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
// for most plugin types, their names are defined in core_plugin lang file
return get_string('type_' . $type, 'core_plugin');
} else if ($parent = $this->get_parent_of_subplugin($type)) {
// if this is a subplugin, try to ask the parent plugin for the name
if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
} else {
return $this->plugin_name($parent) . ' / ' . $type;
}
} else {
return $type;
}
}
/**
* Returns a localized name of a plugin type in plural form
*
@ -570,9 +609,10 @@ class plugin_manager {
),
'tool' => array(
'assignmentupgrade', 'behat', 'capability', 'customlang', 'dbtransfer',
'generator', 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit',
'profiling', 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
'assignmentupgrade', 'behat', 'capability', 'customlang',
'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
'langimport', 'multilangupgrade', 'phpunit', 'profiling',
'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
'unittest', 'uploaduser', 'unsuproles', 'xmldb'
),

View File

@ -18,14 +18,40 @@
/**
* Moodle deployment utility
*
* This script looks after deploying available updates to the local Moodle site.
* This script looks after deploying new add-ons and available updates for them
* to the local Moodle site. It can operate via both HTTP and CLI mode.
* Moodle itself calls this utility via the HTTP mode when the admin is about to
* install or update an add-on. You can use the CLI mode in your custom deployment
* shell scripts.
*
* CLI usage example:
*
* $ sudo -u apache php mdeploy.php --install \
* --package=https://moodle.org/plugins/download.php/...zip \
* --typeroot=/var/www/moodle/htdocs/blocks
* --name=loancalc
* --md5=...
* --dataroot=/var/www/moodle/data
*
* $ sudo -u apache php mdeploy.php --upgrade \
* --package=https://moodle.org/plugins/download.php/...zip \
* --dataroot=/home/mudrd8mz/moodledata/moodle24
* --typeroot=/var/www/moodle/htdocs/blocks
* --name=loancalc
* --md5=...
* --dataroot=/var/www/moodle/data
*
* When called via HTTP, additional parameters returnurl, passfile and password must be
* provided. Optional proxy configuration can be passed using parameters proxy, proxytype
* and proxyuserpwd.
*
* Changes
*
* 1.1 - Added support to install a new plugin from the Moodle Plugins directory.
* 1.0 - Initial version used in Moodle 2.4 to deploy available updates.
*
* @package core
* @subpackage mdeploy
* @version 1.1
* @copyright 2012 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -775,7 +801,53 @@ class worker extends singleton_pattern {
$this->done();
} else if ($this->input->get_option('install')) {
// Installing a new plugin not implemented yet.
$this->log('Plugin installation requested');
$plugintyperoot = $this->input->get_option('typeroot');
$pluginname = $this->input->get_option('name');
$source = $this->input->get_option('package');
$md5remote = $this->input->get_option('md5');
// Check if the plugin location if available for us.
$pluginlocation = $plugintyperoot.'/'.$pluginname;
$this->log('New plugin code location: '.$pluginlocation);
if (file_exists($pluginlocation)) {
throw new filesystem_exception('Unable to prepare the plugin location (directory already exists)');
}
if (!$this->create_directory_precheck($pluginlocation)) {
throw new filesystem_exception('Unable to prepare the plugin location (cannot create new directory)');
}
// Fetch the ZIP file into a temporary location.
$target = $this->target_location($source);
$this->log('Downloading package '.$source);
if ($this->download_file($source, $target)) {
$this->log('Package downloaded into '.$target);
} else {
$this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror);
$this->log('Unable to download the file');
throw new download_file_exception('Unable to download the package');
}
// Compare MD5 checksum of the ZIP file
$md5local = md5_file($target);
if ($md5local !== $md5remote) {
$this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local);
throw new checksum_exception('MD5 checksum failed');
}
$this->log('MD5 checksum ok');
// Unzip the plugin package file into the plugin location.
$this->unzip_plugin($target, $plugintyperoot, $pluginlocation, false);
$this->log('Package successfully extracted');
// Redirect to the given URL (in HTTP) or exit (in CLI).
$this->done();
}
// Print help in CLI by default.
@ -1126,18 +1198,36 @@ class worker extends singleton_pattern {
}
/**
* Checks to see if a source foldr could be safely moved into the given new location
* Checks to see if a source folder could be safely moved into the given new location
*
* @param string $destination full path to the new expected location of a folder
* @return bool
*/
protected function move_directory_target_precheck($target) {
if (file_exists($target)) {
// Check if the target folder does not exist yet, can be created
// and removed again.
$result = $this->create_directory_precheck($target);
// At the moment, it seems to be enough to check. We may want to add
// more steps in the future.
return $result;
}
/**
* Make sure the given directory can be created (and removed)
*
* @param string $path full path to the folder
* @return bool
*/
protected function create_directory_precheck($path) {
if (file_exists($path)) {
return false;
}
$result = mkdir($target, 02777) && rmdir($target);
$result = mkdir($path, 02777) && rmdir($path);
return $result;
}
@ -1265,7 +1355,7 @@ class worker extends singleton_pattern {
* @param string $ziplocation full path to the ZIP file
* @param string $plugintyperoot full path to the plugin's type location
* @param string $expectedlocation expected full path to the plugin after it is extracted
* @param string $backuplocation location of the previous version of the plugin
* @param string|bool $backuplocation location of the previous version of the plugin or false for no backup
*/
protected function unzip_plugin($ziplocation, $plugintyperoot, $expectedlocation, $backuplocation) {
@ -1273,7 +1363,9 @@ class worker extends singleton_pattern {
$result = $zip->open($ziplocation);
if ($result !== true) {
$this->move_directory($backuplocation, $expectedlocation);
if ($backuplocation !== false) {
$this->move_directory($backuplocation, $expectedlocation);
}
throw new zip_exception('Unable to open the zip package');
}
@ -1292,7 +1384,9 @@ class worker extends singleton_pattern {
if (!$zip->extractTo($plugintyperoot)) {
$zip->close();
$this->remove_directory($expectedlocation, true); // just in case something was created
$this->move_directory_into($backuplocation, $expectedlocation);
if ($backuplocation !== false) {
$this->move_directory_into($backuplocation, $expectedlocation);
}
throw new zip_exception('Unable to extract the zip package');
}

View File

@ -106,6 +106,13 @@ class testable_worker extends worker {
public function remove_directory($path, $keeppathroot = false) {
return parent::remove_directory($path, $keeppathroot);
}
/**
* Provides access to the protected method.
*/
public function create_directory_precheck($path) {
return parent::create_directory_precheck($path);
}
}
@ -284,4 +291,13 @@ class mdeploytest extends PHPUnit_Framework_TestCase {
$this->assertTrue($worker->remove_directory($root.'/c'));
$this->assertFalse(is_dir($root.'/c'));
}
public function test_create_directory_precheck() {
$worker = testable_worker::instance();
$root = sys_get_temp_dir().'/'.uniqid('mdeploytest', true);
$this->assertFalse(file_exists($root));
$this->assertTrue($worker->create_directory_precheck($root));
$this->assertFalse(file_exists($root)); // The precheck is supposed to remove it again.
}
}