diff --git a/admin/renderer.php b/admin/renderer.php index dc6172517cb..b8c713abae9 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -241,62 +241,6 @@ class core_admin_renderer extends plugin_renderer_base { return $output; } - /** - * Prints a page with a summary of plugin deployment to be confirmed. - * - * @param \core\update\deployer $deployer - * @param array $data deployer's data package as returned by {@link \core\update\deployer::submitted_data()} - * @return string - */ - public function upgrade_plugin_confirm_deploy_page(\core\update\deployer $deployer, array $data) { - - if (!$deployer->initialized()) { - throw new coding_exception('Unable to render a page for non-initialized deployer.'); - } - - if (empty($data['updateinfo'])) { - throw new coding_exception('Missing required data component.'); - } - - $updateinfo = $data['updateinfo']; - - $output = ''; - $output .= $this->header(); - $output .= $this->container_start('generalbox updateplugin', 'notice'); - - $a = new stdClass(); - if (get_string_manager()->string_exists('pluginname', $updateinfo->component)) { - $a->name = get_string('pluginname', $updateinfo->component); - } else { - $a->name = $updateinfo->component; - } - - if (isset($updateinfo->release)) { - $a->version = $updateinfo->release . ' (' . $updateinfo->version . ')'; - } else { - $a->version = $updateinfo->version; - } - $a->url = $updateinfo->download; - - $output .= $this->output->heading(get_string('updatepluginconfirm', 'core_plugin')); - $output .= $this->output->container(format_text(get_string('updatepluginconfirminfo', 'core_plugin', $a)), 'updatepluginconfirminfo'); - $output .= $this->output->container(get_string('updatepluginconfirmwarning', 'core_plugin', 'updatepluginconfirmwarning')); - - if ($repotype = $deployer->plugin_external_source($data['updateinfo'])) { - $output .= $this->output->container(get_string('updatepluginconfirmexternal', 'core_plugin', $repotype), 'updatepluginconfirmexternal'); - } - - $widget = $deployer->make_execution_widget($data['updateinfo'], $data['returnurl']); - $output .= $this->output->render($widget); - - $output .= $this->output->single_button($data['callerurl'], get_string('cancel', 'core'), 'get'); - - $output .= $this->container_end(); - $output .= $this->footer(); - - return $output; - } - /** * Display the admin notifications page. * @param int $maturity @@ -981,7 +925,7 @@ class core_admin_renderer extends plugin_renderer_base { $availableupdates = $plugin->available_updates(); if (!empty($availableupdates)) { foreach ($availableupdates as $availableupdate) { - $status .= $this->plugin_available_update_info($availableupdate); + $status .= $this->plugin_available_update_info($pluginman, $availableupdate); } } @@ -1617,7 +1561,7 @@ class core_admin_renderer extends plugin_renderer_base { $updateinfo = ''; if (is_array($plugin->available_updates())) { foreach ($plugin->available_updates() as $availableupdate) { - $updateinfo .= $this->plugin_available_update_info($availableupdate); + $updateinfo .= $this->plugin_available_update_info($pluginman, $availableupdate); } } @@ -1636,12 +1580,10 @@ class core_admin_renderer extends plugin_renderer_base { /** * Helper method to render the information about the available plugin update * - * The passed objects always provides at least the 'version' property containing - * the (higher) version of the plugin available. - * + * @param core_plugin_manager $pluginman plugin manager instance * @param \core\update\info $updateinfo information about the available update for the plugin */ - protected function plugin_available_update_info(\core\update\info $updateinfo) { + protected function plugin_available_update_info(core_plugin_manager $pluginman, \core\update\info $updateinfo) { $boxclasses = 'pluginupdateinfo'; $info = array(); @@ -1670,19 +1612,18 @@ class core_admin_renderer extends plugin_renderer_base { $box .= html_writer::tag('div', get_string('updateavailable', 'core_plugin', $updateinfo->version), array('class' => 'version')); $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), ''); - $deployer = \core\update\deployer::instance(); - if ($deployer->initialized()) { - $impediments = $deployer->deployment_impediments($updateinfo); - if (empty($impediments)) { - $widget = $deployer->make_confirm_widget($updateinfo); - $box .= $this->output->render($widget); - } else { - if (isset($impediments['notwritable'])) { - $box .= $this->output->help_icon('notwritable', 'core_plugin', get_string('notwritable', 'core_plugin')); - } - if (isset($impediments['notdownloadable'])) { - $box .= $this->output->help_icon('notdownloadable', 'core_plugin', get_string('notdownloadable', 'core_plugin')); - } + if ($pluginman->is_remote_plugin_installable($updateinfo->component, $updateinfo->version, $reason)) { + $box .= $this->output->single_button( + new moodle_url($this->page->url, array('installupdate' => $updateinfo->component, + 'installupdateversion' => $updateinfo->version)), + get_string('updateavailableinstall', 'core_admin'), + 'post', + array('class' => 'singlebutton updateavailableinstall') + ); + } else { + $reasonhelp = $this->info_remote_plugin_not_installable($reason); + if ($reasonhelp) { + $box .= html_writer::div($reasonhelp, 'reasonhelp updateavailableinstall'); } } diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index 7059010b338..6b4061d5e27 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -348,6 +348,8 @@ class tool_installaddon_installer { * Moodle itself, tuned for fetching files from moodle.org servers. Same code is used * in mdeploy.php for fetching available updates. * + * TODO This all will be rewritten to use new plugin manager features. + * * @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 diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index f75cb0e5b0b..02bbedb78cf 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -667,7 +667,6 @@ class core_plugin_manager { /** * Check to see if the current version of the plugin seems to be a checkout of an external repository. * - * @see \core\update\deployer::plugin_external_source() * @param string $component frankenstyle component name * @return false|string */ diff --git a/lib/classes/update/deployer.php b/lib/classes/update/deployer.php deleted file mode 100644 index 25ba1683081..00000000000 --- a/lib/classes/update/deployer.php +++ /dev/null @@ -1,574 +0,0 @@ -. - -/** - * Defines classes used for updates. - * - * @package core - * @copyright 2011 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -namespace core\update; - -use coding_exception, core_component, moodle_url; - -defined('MOODLE_INTERNAL') || die(); - -/** - * Implements a communication bridge to the mdeploy.php utility - */ -class deployer { - - /** @var \core\update\deployer holds the singleton instance */ - protected static $singletoninstance; - /** @var moodle_url URL of a page that includes the deployer UI */ - protected $callerurl; - /** @var moodle_url URL to return after the deployment */ - protected $returnurl; - - /** - * Direct instantiation not allowed, use the factory method {@link self::instance()} - */ - protected function __construct() { - } - - /** - * Sorry, this is singleton - */ - protected function __clone() { - } - - /** - * Factory method for this class - * - * @return \core\update\deployer the singleton instance - */ - public static function instance() { - if (is_null(self::$singletoninstance)) { - self::$singletoninstance = new self(); - } - return self::$singletoninstance; - } - - /** - * Reset caches used by this script - * - * @param bool $phpunitreset is this called as a part of PHPUnit reset? - */ - public static function reset_caches($phpunitreset = false) { - if ($phpunitreset) { - self::$singletoninstance = null; - } - } - - /** - * Is automatic deployment enabled? - * - * @return bool - */ - public function enabled() { - global $CFG; - - if (!empty($CFG->disableupdateautodeploy)) { - // The feature is prohibited via config.php. - return false; - } - - return get_config('updateautodeploy'); - } - - /** - * Sets some base properties of the class to make it usable. - * - * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data - * @param moodle_url $returnurl the final URL to return to when the deployment is finished - */ - public function initialize(moodle_url $callerurl, moodle_url $returnurl) { - - if (!$this->enabled()) { - throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.'); - } - - $this->callerurl = $callerurl; - $this->returnurl = $returnurl; - } - - /** - * Has the deployer been initialized? - * - * Initialized deployer means that the following properties were set: - * callerurl, returnurl - * - * @return bool - */ - public function initialized() { - - if (!$this->enabled()) { - return false; - } - - if (empty($this->callerurl)) { - return false; - } - - if (empty($this->returnurl)) { - return false; - } - - return true; - } - - /** - * Returns a list of reasons why the deployment can not happen - * - * If the returned array is empty, the deployment seems to be possible. The returned - * structure is an associative array with keys representing individual impediments. - * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable. - * - * @param \core\update\info $info - * @return array - */ - public function deployment_impediments(info $info) { - - $impediments = array(); - - if (empty($info->download)) { - $impediments['missingdownloadurl'] = true; - } - - if (empty($info->downloadmd5)) { - $impediments['missingdownloadmd5'] = true; - } - - if (!empty($info->download) and !$this->update_downloadable($info->download)) { - $impediments['notdownloadable'] = true; - } - - if (!$this->component_writable($info->component)) { - $impediments['notwritable'] = true; - } - - return $impediments; - } - - /** - * Check to see if the current version of the plugin seems to be a checkout of an external repository. - * - * @see core_plugin_manager::plugin_external_source() - * @param \core\update\info $info - * @return false|string - */ - public function plugin_external_source(info $info) { - - $paths = core_component::get_plugin_types(); - list($plugintype, $pluginname) = core_component::normalize_component($info->component); - $pluginroot = $paths[$plugintype].'/'.$pluginname; - - if (is_dir($pluginroot.'/.git')) { - return 'git'; - } - - if (is_file($pluginroot.'/.git')) { - return 'git-submodule'; - } - - if (is_dir($pluginroot.'/CVS')) { - return 'cvs'; - } - - if (is_dir($pluginroot.'/.svn')) { - return 'svn'; - } - - if (is_dir($pluginroot.'/.hg')) { - return 'mercurial'; - } - - return false; - } - - /** - * Prepares a renderable widget to confirm installation of an available update. - * - * @param \core\update\info $info component version to deploy - * @return \renderable - */ - public function make_confirm_widget(info $info) { - - if (!$this->initialized()) { - throw new coding_exception('Illegal method call - deployer not initialized.'); - } - - $params = array( - 'updateaddon' => $info->component, - 'version' =>$info->version, - 'sesskey' => sesskey(), - ); - - // Append some our own data. - if (!empty($this->callerurl)) { - $params['callerurl'] = $this->callerurl->out(false); - } - if (!empty($this->returnurl)) { - $params['returnurl'] = $this->returnurl->out(false); - } - - $widget = new \single_button( - new moodle_url($this->callerurl, $params), - get_string('updateavailableinstall', 'core_admin'), - 'post' - ); - - return $widget; - } - - /** - * Prepares a renderable widget to execute installation of an available update. - * - * @param \core\update\info $info component version to deploy - * @param moodle_url $returnurl URL to return after the installation execution - * @return \renderable - */ - public function make_execution_widget(info $info, moodle_url $returnurl = null) { - global $CFG; - - if (!$this->initialized()) { - throw new coding_exception('Illegal method call - deployer not initialized.'); - } - - $pluginrootpaths = core_component::get_plugin_types(); - - list($plugintype, $pluginname) = core_component::normalize_component($info->component); - - if (empty($pluginrootpaths[$plugintype])) { - throw new coding_exception('Unknown plugin type root location', $plugintype); - } - - list($passfile, $password) = $this->prepare_authorization(); - - if (is_null($returnurl)) { - $returnurl = new moodle_url('/admin'); - } else { - $returnurl = $returnurl; - } - - $params = array( - 'upgrade' => true, - 'type' => $plugintype, - 'name' => $pluginname, - 'typeroot' => $pluginrootpaths[$plugintype], - 'package' => $info->download, - 'md5' => $info->downloadmd5, - 'dataroot' => $CFG->dataroot, - 'dirroot' => $CFG->dirroot, - 'passfile' => $passfile, - 'password' => $password, - 'returnurl' => $returnurl->out(false), - ); - - if (!empty($CFG->proxyhost)) { - // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our - // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy - // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is - // fixed, the condition should be amended. - if (true or !is_proxybypass($info->download)) { - if (empty($CFG->proxyport)) { - $params['proxy'] = $CFG->proxyhost; - } else { - $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport; - } - - if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) { - $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword; - } - - if (!empty($CFG->proxytype)) { - $params['proxytype'] = $CFG->proxytype; - } - } - } - - $widget = new \single_button( - new moodle_url('/mdeploy.php', $params), - get_string('updateavailableinstall', 'core_admin'), - 'post' - ); - - return $widget; - } - - /** - * Returns array of data objects passed to this tool. - * - * @return array - */ - public function submitted_data() { - $component = optional_param('updateaddon', '', PARAM_COMPONENT); - $version = optional_param('version', '', PARAM_RAW); - if (!$component or !$version) { - return false; - } - - $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component); - if (!$plugininfo) { - return false; - } - - if ($plugininfo->is_standard()) { - return false; - } - - if (!$updates = $plugininfo->available_updates()) { - return false; - } - - $info = null; - foreach ($updates as $update) { - if ($update->version == $version) { - $info = $update; - break; - } - } - if (!$info) { - return false; - } - - $data = array( - 'updateaddon' => $component, - 'updateinfo' => $info, - 'callerurl' => optional_param('callerurl', null, PARAM_URL), - 'returnurl' => optional_param('returnurl', null, PARAM_URL), - ); - if ($data['callerurl']) { - $data['callerurl'] = new moodle_url($data['callerurl']); - } - if ($data['callerurl']) { - $data['returnurl'] = new moodle_url($data['returnurl']); - } - - return $data; - } - - /** - * Handles magic getters and setters for protected properties. - * - * @param string $name method name, e.g. set_returnurl() - * @param array $arguments arguments to be passed to the array - */ - public function __call($name, array $arguments = array()) { - - if (substr($name, 0, 4) === 'set_') { - $property = substr($name, 4); - if (empty($property)) { - throw new coding_exception('Invalid property name (empty)'); - } - if (empty($arguments)) { - $arguments = array(true); // Default value for flag-like properties. - } - // Make sure it is a protected property. - $isprotected = false; - $reflection = new \ReflectionObject($this); - foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) { - if ($reflectionproperty->getName() === $property) { - $isprotected = true; - break; - } - } - if (!$isprotected) { - throw new coding_exception('Unable to set property - it does not exist or it is not protected'); - } - $value = reset($arguments); - $this->$property = $value; - return; - } - - if (substr($name, 0, 4) === 'get_') { - $property = substr($name, 4); - if (empty($property)) { - throw new coding_exception('Invalid property name (empty)'); - } - if (!empty($arguments)) { - throw new coding_exception('No parameter expected'); - } - // Make sure it is a protected property. - $isprotected = false; - $reflection = new \ReflectionObject($this); - foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) { - if ($reflectionproperty->getName() === $property) { - $isprotected = true; - break; - } - } - if (!$isprotected) { - throw new coding_exception('Unable to get property - it does not exist or it is not protected'); - } - return $this->$property; - } - } - - /** - * Generates a random token and stores it in a file in moodledata directory. - * - * @return array of the (string)filename and (string)password in this order - */ - public function prepare_authorization() { - global $CFG; - - make_upload_directory('mdeploy/auth/'); - - $attempts = 0; - $success = false; - - while (!$success and $attempts < 5) { - $attempts++; - - $passfile = $this->generate_passfile(); - $password = $this->generate_password(); - $now = time(); - - $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile; - - if (!file_exists($filepath)) { - $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX); - chmod($filepath, $CFG->filepermissions); - } - } - - if ($success) { - return array($passfile, $password); - - } else { - throw new \moodle_exception('unable_prepare_authorization', 'core_plugin'); - } - } - - /* === End of external API === */ - - /** - * Returns a random string to be used as a filename of the password storage. - * - * @return string - */ - protected function generate_passfile() { - return clean_param(uniqid('mdeploy_', true), PARAM_FILE); - } - - /** - * Returns a random string to be used as the authorization token - * - * @return string - */ - protected function generate_password() { - return complex_random_string(); - } - - /** - * Checks if the given component's directory is writable - * - * For the purpose of the deployment, the web server process has to have - * write access to all files in the component's directory (recursively) and for the - * directory itself. - * - * @see worker::move_directory_source_precheck() - * @param string $component normalized component name - * @return boolean - */ - protected function component_writable($component) { - - list($plugintype, $pluginname) = core_component::normalize_component($component); - - $directory = core_component::get_plugin_directory($plugintype, $pluginname); - - if (is_null($directory)) { - // Plugin unknown, most probably deleted or missing during upgrade, - // look at the parent directory instead because they might want to install it. - $plugintypes = core_component::get_plugin_types(); - if (!isset($plugintypes[$plugintype])) { - throw new coding_exception('Unknown component location', $component); - } - $directory = $plugintypes[$plugintype]; - } - - return $this->directory_writable($directory); - } - - /** - * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL - * - * This is mainly supposed to check if the transmission over HTTPS would - * work. That is, if the CA certificates are present at the server. - * - * @param string $downloadurl the URL of the ZIP package to download - * @return bool - */ - protected function update_downloadable($downloadurl) { - global $CFG; - - $curloptions = array( - 'CURLOPT_SSL_VERIFYHOST' => 2, // This is the default in {@link curl} class but just in case. - 'CURLOPT_SSL_VERIFYPEER' => true, - ); - - $curl = new \curl(array('proxy' => true)); - $result = $curl->head($downloadurl, $curloptions); - $errno = $curl->get_errno(); - if (empty($errno)) { - return true; - } else { - return false; - } - } - - /** - * Checks if the directory and all its contents (recursively) is writable - * - * @param string $path full path to a directory - * @return boolean - */ - private function directory_writable($path) { - - if (!is_writable($path)) { - return false; - } - - if (is_dir($path)) { - $handle = opendir($path); - } else { - return false; - } - - $result = true; - - while ($filename = readdir($handle)) { - $filepath = $path.'/'.$filename; - - if ($filename === '.' or $filename === '..') { - continue; - } - - if (is_dir($filepath)) { - $result = $result && $this->directory_writable($filepath); - - } else { - $result = $result && is_writable($filepath); - } - } - - closedir($handle); - - return $result; - } -} diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index cc1c64a3e04..a3d7233054b 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -240,9 +240,6 @@ class phpunit_util extends testing_util { if (class_exists('\core\update\checker')) { \core\update\checker::reset_caches(true); } - if (class_exists('\core\update\deployer')) { - \core\update\deployer::reset_caches(true); - } // Clear static cache within restore. if (class_exists('restore_section_structure_step')) { diff --git a/mdeploy.php b/mdeploy.php deleted file mode 100644 index 26c5caa1970..00000000000 --- a/mdeploy.php +++ /dev/null @@ -1,1574 +0,0 @@ -. - -/** - * Moodle deployment utility - * - * 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=... - * - * $ sudo -u apache php mdeploy.php --upgrade \ - * --package=https://moodle.org/plugins/download.php/...zip \ - * --typeroot=/var/www/moodle/htdocs/blocks - * --name=loancalc - * --md5=... - * - * 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 - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -if (defined('MOODLE_INTERNAL')) { - die('This is a standalone utility that should not be included by any other Moodle code.'); -} - -// This stops immediately at the beginning of lib/setup.php. -define('ABORT_AFTER_CONFIG', true); -if (PHP_SAPI === 'cli') { - // Called from the CLI - we need to set CLI_SCRIPT to ensure that appropriate CLI checks are made in setup.php. - define('CLI_SCRIPT', true); -} -require(__DIR__ . '/config.php'); - -// Exceptions ////////////////////////////////////////////////////////////////// - -class invalid_coding_exception extends Exception {} -class missing_option_exception extends Exception {} -class invalid_option_exception extends Exception {} -class unauthorized_access_exception extends Exception {} -class download_file_exception extends Exception {} -class backup_folder_exception extends Exception {} -class zip_exception extends Exception {} -class filesystem_exception extends Exception {} -class checksum_exception extends Exception {} -class invalid_setting_exception extends Exception {} - - -// Various support classes ///////////////////////////////////////////////////// - -/** - * Base class implementing the singleton pattern using late static binding feature. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class singleton_pattern { - - /** @var array singleton_pattern instances */ - protected static $singletoninstances = array(); - - /** - * Factory method returning the singleton instance. - * - * Subclasses may want to override the {@link self::initialize()} method that is - * called right after their instantiation. - * - * @return mixed the singleton instance - */ - final public static function instance() { - $class = get_called_class(); - if (!isset(static::$singletoninstances[$class])) { - static::$singletoninstances[$class] = new static(); - static::$singletoninstances[$class]->initialize(); - } - return static::$singletoninstances[$class]; - } - - /** - * Optional post-instantiation code. - */ - protected function initialize() { - // Do nothing in this base class. - } - - /** - * Direct instantiation not allowed, use the factory method {@link instance()} - */ - final protected function __construct() { - } - - /** - * Sorry, this is singleton. - */ - final protected function __clone() { - } -} - - -// User input handling ///////////////////////////////////////////////////////// - -/** - * Provides access to the script options. - * - * Implements the delegate pattern by dispatching the calls to appropriate - * helper class (CLI or HTTP). - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class input_manager extends singleton_pattern { - - const TYPE_FILE = 'file'; // File name - const TYPE_FLAG = 'flag'; // No value, just a flag (switch) - const TYPE_INT = 'int'; // Integer - const TYPE_PATH = 'path'; // Full path to a file or a directory - const TYPE_RAW = 'raw'; // Raw value, keep as is - const TYPE_URL = 'url'; // URL to a file - const TYPE_PLUGIN = 'plugin'; // Plugin name - const TYPE_MD5 = 'md5'; // MD5 hash - - /** @var input_cli_provider|input_http_provider the provider of the input */ - protected $inputprovider = null; - - /** - * Returns the value of an option passed to the script. - * - * If the caller passes just the $name, the requested argument is considered - * required. The caller may specify the second argument which then - * makes the argument optional with the given default value. - * - * If the type of the $name option is TYPE_FLAG (switch), this method returns - * true if the flag has been passed or false if it was not. Specifying the - * default value makes no sense in this case and leads to invalid coding exception. - * - * The array options are not supported. - * - * @example $filename = $input->get_option('f'); - * @example $filename = $input->get_option('filename'); - * @example if ($input->get_option('verbose')) { ... } - * @param string $name - * @return mixed - */ - public function get_option($name, $default = 'provide_default_value_explicitly') { - - $this->validate_option_name($name); - - $info = $this->get_option_info($name); - - if ($info->type === input_manager::TYPE_FLAG) { - return $this->inputprovider->has_option($name); - } - - if (func_num_args() == 1) { - return $this->get_required_option($name); - } else { - return $this->get_optional_option($name, $default); - } - } - - /** - * Returns the meta-information about the given option. - * - * @param string|null $name short or long option name, defaults to returning the list of all - * @return array|object|false array with all, object with the specific option meta-information or false of no such an option - */ - public function get_option_info($name=null) { - - $supportedoptions = array( - array('', 'passfile', input_manager::TYPE_FILE, 'File name of the passphrase file (HTTP access only)'), - array('', 'password', input_manager::TYPE_RAW, 'Session passphrase (HTTP access only)'), - array('', 'proxy', input_manager::TYPE_RAW, 'HTTP proxy host and port (e.g. \'our.proxy.edu:8888\')'), - array('', 'proxytype', input_manager::TYPE_RAW, 'Proxy type (HTTP or SOCKS5)'), - array('', 'proxyuserpwd', input_manager::TYPE_RAW, 'Proxy username and password (e.g. \'username:password\')'), - array('', 'returnurl', input_manager::TYPE_URL, 'Return URL (HTTP access only)'), - array('h', 'help', input_manager::TYPE_FLAG, 'Prints usage information'), - array('i', 'install', input_manager::TYPE_FLAG, 'Installation mode'), - array('m', 'md5', input_manager::TYPE_MD5, 'Expected MD5 hash of the ZIP package to deploy'), - array('n', 'name', input_manager::TYPE_PLUGIN, 'Plugin name (the name of its folder)'), - array('p', 'package', input_manager::TYPE_URL, 'URL to the ZIP package to deploy'), - array('r', 'typeroot', input_manager::TYPE_PATH, 'Full path of the container for this plugin type'), - array('u', 'upgrade', input_manager::TYPE_FLAG, 'Upgrade mode'), - ); - - if (is_null($name)) { - $all = array(); - foreach ($supportedoptions as $optioninfo) { - $info = new stdClass(); - $info->shortname = $optioninfo[0]; - $info->longname = $optioninfo[1]; - $info->type = $optioninfo[2]; - $info->desc = $optioninfo[3]; - $all[] = $info; - } - return $all; - } - - $found = false; - - foreach ($supportedoptions as $optioninfo) { - if (strlen($name) == 1) { - // Search by the short option name - if ($optioninfo[0] === $name) { - $found = $optioninfo; - break; - } - } else { - // Search by the long option name - if ($optioninfo[1] === $name) { - $found = $optioninfo; - break; - } - } - } - - if (!$found) { - return false; - } - - $info = new stdClass(); - $info->shortname = $found[0]; - $info->longname = $found[1]; - $info->type = $found[2]; - $info->desc = $found[3]; - - return $info; - } - - /** - * Casts the value to the given type. - * - * @param mixed $raw the raw value - * @param string $type the expected value type, e.g. {@link input_manager::TYPE_INT} - * @return mixed - */ - public function cast_value($raw, $type) { - - if (is_array($raw)) { - throw new invalid_coding_exception('Unsupported array option.'); - } else if (is_object($raw)) { - throw new invalid_coding_exception('Unsupported object option.'); - } - - switch ($type) { - - case input_manager::TYPE_FILE: - $raw = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $raw); - $raw = preg_replace('~\.\.+~', '', $raw); - if ($raw === '.') { - $raw = ''; - } - return $raw; - - case input_manager::TYPE_FLAG: - return true; - - case input_manager::TYPE_INT: - return (int)$raw; - - case input_manager::TYPE_PATH: - if (strpos($raw, '~') !== false) { - throw new invalid_option_exception('Using the tilde (~) character in paths is not supported'); - } - $colonpos = strpos($raw, ':'); - if ($colonpos !== false) { - if ($colonpos !== 1 or strrpos($raw, ':') !== 1) { - throw new invalid_option_exception('Using the colon (:) character in paths is supported for Windows drive labels only.'); - } - if (preg_match('/^[a-zA-Z]:/', $raw) !== 1) { - throw new invalid_option_exception('Using the colon (:) character in paths is supported for Windows drive labels only.'); - } - } - $raw = str_replace('\\', '/', $raw); - $raw = preg_replace('~[[:cntrl:]]|[&<>"`\|\']~u', '', $raw); - $raw = preg_replace('~\.\.+~', '', $raw); - $raw = preg_replace('~//+~', '/', $raw); - $raw = preg_replace('~/(\./)+~', '/', $raw); - return $raw; - - case input_manager::TYPE_RAW: - return $raw; - - case input_manager::TYPE_URL: - $regex = '^(https?|ftp)\:\/\/'; // protocol - $regex .= '([a-z0-9+!*(),;?&=\$_.-]+(\:[a-z0-9+!*(),;?&=\$_.-]+)?@)?'; // optional user and password - $regex .= '[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)*'; // hostname or IP (one word like http://localhost/ allowed) - $regex .= '(\:[0-9]{2,5})?'; // port (optional) - $regex .= '(\/([a-z0-9+\$_-]\.?)+)*\/?'; // path to the file - $regex .= '(\?[a-z+&\$_.-][a-z0-9;:@/&%=+\$_.-]*)?'; // HTTP params - - if (preg_match('#'.$regex.'#i', $raw)) { - return $raw; - } else { - throw new invalid_option_exception('Not a valid URL'); - } - - case input_manager::TYPE_PLUGIN: - if (!preg_match('/^[a-z][a-z0-9_]*[a-z0-9]$/', $raw)) { - throw new invalid_option_exception('Invalid plugin name'); - } - if (strpos($raw, '__') !== false) { - throw new invalid_option_exception('Invalid plugin name'); - } - return $raw; - - case input_manager::TYPE_MD5: - if (!preg_match('/^[a-f0-9]{32}$/', $raw)) { - throw new invalid_option_exception('Invalid MD5 hash format'); - } - return $raw; - - default: - throw new invalid_coding_exception('Unknown option type.'); - - } - } - - /** - * Picks the appropriate helper class to delegate calls to. - */ - protected function initialize() { - if (PHP_SAPI === 'cli') { - $this->inputprovider = input_cli_provider::instance(); - } else { - $this->inputprovider = input_http_provider::instance(); - } - } - - // End of external API - - /** - * Validates the parameter name. - * - * @param string $name - * @throws invalid_coding_exception - */ - protected function validate_option_name($name) { - - if (empty($name)) { - throw new invalid_coding_exception('Invalid empty option name.'); - } - - $meta = $this->get_option_info($name); - if (empty($meta)) { - throw new invalid_coding_exception('Invalid option name: '.$name); - } - } - - /** - * Returns cleaned option value or throws exception. - * - * @param string $name the name of the parameter - * @param string $type the parameter type, e.g. {@link input_manager::TYPE_INT} - * @return mixed - */ - protected function get_required_option($name) { - if ($this->inputprovider->has_option($name)) { - return $this->inputprovider->get_option($name); - } else { - throw new missing_option_exception('Missing required option: '.$name); - } - } - - /** - * Returns cleaned option value or the default value - * - * @param string $name the name of the parameter - * @param string $type the parameter type, e.g. {@link input_manager::TYPE_INT} - * @param mixed $default the default value. - * @return mixed - */ - protected function get_optional_option($name, $default) { - if ($this->inputprovider->has_option($name)) { - return $this->inputprovider->get_option($name); - } else { - return $default; - } - } -} - - -/** - * Base class for input providers. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class input_provider extends singleton_pattern { - - /** @var array list of all passed valid options */ - protected $options = array(); - - /** - * Returns the casted value of the option. - * - * @param string $name option name - * @throws invalid_coding_exception if the option has not been passed - * @return mixed casted value of the option - */ - public function get_option($name) { - - if (!$this->has_option($name)) { - throw new invalid_coding_exception('Option not passed: '.$name); - } - - return $this->options[$name]; - } - - /** - * Was the given option passed? - * - * @param string $name optionname - * @return bool - */ - public function has_option($name) { - return array_key_exists($name, $this->options); - } - - /** - * Initializes the input provider. - */ - protected function initialize() { - $this->populate_options(); - } - - // End of external API - - /** - * Parses and validates all supported options passed to the script. - */ - protected function populate_options() { - - $input = input_manager::instance(); - $raw = $this->parse_raw_options(); - $cooked = array(); - - foreach ($raw as $k => $v) { - if (is_array($v) or is_object($v)) { - // Not supported. - } - - $info = $input->get_option_info($k); - if (!$info) { - continue; - } - - $casted = $input->cast_value($v, $info->type); - - if (!empty($info->shortname)) { - $cooked[$info->shortname] = $casted; - } - - if (!empty($info->longname)) { - $cooked[$info->longname] = $casted; - } - } - - // Store the options. - $this->options = $cooked; - } -} - - -/** - * Provides access to the script options passed via CLI. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class input_cli_provider extends input_provider { - - /** - * Parses raw options passed to the script. - * - * @return array as returned by getopt() - */ - protected function parse_raw_options() { - - $input = input_manager::instance(); - - // Signatures of some in-built PHP functions are just crazy, aren't they. - $short = ''; - $long = array(); - - foreach ($input->get_option_info() as $option) { - if ($option->type === input_manager::TYPE_FLAG) { - // No value expected for this option. - $short .= $option->shortname; - $long[] = $option->longname; - } else { - // A value expected for the option, all considered as optional. - $short .= empty($option->shortname) ? '' : $option->shortname.'::'; - $long[] = empty($option->longname) ? '' : $option->longname.'::'; - } - } - - return getopt($short, $long); - } -} - - -/** - * Provides access to the script options passed via HTTP request. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class input_http_provider extends input_provider { - - /** - * Parses raw options passed to the script. - * - * @return array of raw values passed via HTTP request - */ - protected function parse_raw_options() { - return $_POST; - } -} - - -// Output handling ///////////////////////////////////////////////////////////// - -/** - * Provides output operations. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class output_manager extends singleton_pattern { - - /** @var output_cli_provider|output_http_provider the provider of the output functionality */ - protected $outputprovider = null; - - /** - * Magic method triggered when invoking an inaccessible method. - * - * @param string $name method name - * @param array $arguments method arguments - */ - public function __call($name, array $arguments = array()) { - call_user_func_array(array($this->outputprovider, $name), $arguments); - } - - /** - * Picks the appropriate helper class to delegate calls to. - */ - protected function initialize() { - if (PHP_SAPI === 'cli') { - $this->outputprovider = output_cli_provider::instance(); - } else { - $this->outputprovider = output_http_provider::instance(); - } - } -} - - -/** - * Base class for all output providers. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class output_provider extends singleton_pattern { -} - -/** - * Provides output to the command line. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class output_cli_provider extends output_provider { - - /** - * Prints help information in CLI mode. - */ - public function help() { - - $this->outln('mdeploy.php - Moodle (http://moodle.org) deployment utility'); - $this->outln(); - $this->outln('Usage: $ sudo -u apache php mdeploy.php [options]'); - $this->outln(); - $input = input_manager::instance(); - foreach($input->get_option_info() as $info) { - $option = array(); - if (!empty($info->shortname)) { - $option[] = '-'.$info->shortname; - } - if (!empty($info->longname)) { - $option[] = '--'.$info->longname; - } - $this->outln(sprintf('%-20s %s', implode(', ', $option), $info->desc)); - } - } - - // End of external API - - /** - * Writes a text to the STDOUT followed by a new line character. - * - * @param string $text text to print - */ - protected function outln($text='') { - fputs(STDOUT, $text.PHP_EOL); - } -} - - -/** - * Provides HTML output as a part of HTTP response. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class output_http_provider extends output_provider { - - /** - * Prints help on the script usage. - */ - public function help() { - // No help available via HTTP - } - - /** - * Display the information about uncaught exception - * - * @param Exception $e uncaught exception - */ - public function exception(Exception $e) { - - $docslink = 'http://docs.moodle.org/en/admin/mdeploy/'.get_class($e); - $this->start_output(); - echo('

Oops! It did it again

'); - echo('

Moodle deployment utility had a trouble with your request. - See the docs page and the debugging information for more details.

'); - echo('
');
-        echo exception_handlers::format_exception_info($e);
-        echo('
'); - $this->end_output(); - } - - // End of external API - - /** - * Produce the HTML page header - */ - protected function start_output() { - echo ' - - - - - - -
'; - } - - /** - * Produce the HTML page footer - */ - protected function end_output() { - echo '
'; - } -} - -// The main class providing all the functionality ////////////////////////////// - -/** - * The actual worker class implementing the main functionality of the script. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class worker extends singleton_pattern { - - const EXIT_OK = 0; // Success exit code. - const EXIT_HELP = 1; // Explicit help required. - const EXIT_UNKNOWN_ACTION = 127; // Neither -i nor -u provided. - - /** @var input_manager */ - protected $input = null; - - /** @var output_manager */ - protected $output = null; - - /** @var int the most recent cURL error number, zero for no error */ - private $curlerrno = null; - - /** @var string the most recent cURL error message, empty string for no error */ - private $curlerror = null; - - /** @var array|false the most recent cURL request info, if it was successful */ - private $curlinfo = null; - - /** @var string the full path to the log file */ - private $logfile = null; - - /** @var array the whitelisted config options which can be queried. */ - private $validconfigoptions = array( - 'dirroot' => true, - 'dataroot' => true, - ); - - /** - * Main - the one that actually does something - */ - public function execute() { - - $this->log('=== MDEPLOY EXECUTION START ==='); - - // Authorize access. None in CLI. Passphrase in HTTP. - $this->authorize(); - - // Asking for help in the CLI mode. - if ($this->input->get_option('help')) { - $this->output->help(); - $this->done(self::EXIT_HELP); - } - - if ($this->input->get_option('upgrade')) { - $this->log('Plugin upgrade requested'); - - // Fetch the ZIP file into a temporary location. - $source = $this->input->get_option('package'); - $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 from ' . $source . ' into ' . $target); - throw new download_file_exception('Unable to download the package'); - } - - // Compare MD5 checksum of the ZIP file - $md5remote = $this->input->get_option('md5'); - $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'); - - // Check that the specified typeroot is within the current site's dirroot. - $plugintyperoot = $this->input->get_option('typeroot'); - if (strpos(realpath($plugintyperoot), realpath($this->get_env('dirroot'))) !== 0) { - throw new backup_folder_exception('Unable to backup the current version of the plugin (typeroot is invalid)'); - } - - // Backup the current version of the plugin - $pluginname = $this->input->get_option('name'); - $sourcelocation = $plugintyperoot.'/'.$pluginname; - $backuplocation = $this->backup_location($sourcelocation); - - $this->log('Current plugin code location: '.$sourcelocation); - $this->log('Moving the current code into archive: '.$backuplocation); - - if (file_exists($sourcelocation)) { - // We don't want to touch files unless we are pretty sure it would be all ok. - if (!$this->move_directory_source_precheck($sourcelocation)) { - throw new backup_folder_exception('Unable to backup the current version of the plugin (source precheck failed)'); - } - if (!$this->move_directory_target_precheck($backuplocation)) { - throw new backup_folder_exception('Unable to backup the current version of the plugin (backup precheck failed)'); - } - - // Looking good, let's try it. - if (!$this->move_directory($sourcelocation, $backuplocation, true)) { - throw new backup_folder_exception('Unable to backup the current version of the plugin (moving failed)'); - } - - } else { - // Upgrading missing plugin - this happens often during upgrades. - if (!$this->create_directory_precheck($sourcelocation)) { - throw new filesystem_exception('Unable to prepare the plugin location (cannot create new directory)'); - } - } - - // Unzip the plugin package file into the target location. - $this->unzip_plugin($target, $plugintyperoot, $sourcelocation, $backuplocation); - $this->log('Package successfully extracted'); - - // Redirect to the given URL (in HTTP) or exit (in CLI). - $this->done(); - - } else if ($this->input->get_option('install')) { - $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'); - - if (strpos(realpath($plugintyperoot), realpath($this->get_env('dirroot'))) !== 0) { - throw new backup_folder_exception('Unable to prepare the plugin location (typeroot is invalid)'); - } - - // 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. - $this->output->help(); - $this->done(self::EXIT_UNKNOWN_ACTION); - } - - /** - * Attempts to log a thrown exception - * - * @param Exception $e uncaught exception - */ - public function log_exception(Exception $e) { - $this->log($e->__toString()); - } - - /** - * Initialize the worker class. - */ - protected function initialize() { - $this->input = input_manager::instance(); - $this->output = output_manager::instance(); - } - - // End of external API - - /** - * Finish this script execution. - * - * @param int $exitcode - */ - protected function done($exitcode = self::EXIT_OK) { - - if (PHP_SAPI === 'cli') { - exit($exitcode); - - } else { - $returnurl = $this->input->get_option('returnurl'); - $this->redirect($returnurl); - exit($exitcode); - } - } - - /** - * Authorize access to the script. - * - * In CLI mode, the access is automatically authorized. In HTTP mode, the - * passphrase submitted via the request params must match the contents of the - * file, the name of which is passed in another parameter. - * - * @throws unauthorized_access_exception - */ - protected function authorize() { - if (PHP_SAPI === 'cli') { - $this->log('Successfully authorized using the CLI SAPI'); - return; - } - - $passfile = $this->input->get_option('passfile'); - $password = $this->input->get_option('password'); - - $passpath = $this->get_env('dataroot') . '/mdeploy/auth/' . $passfile; - - if (!is_readable($passpath)) { - throw new unauthorized_access_exception('Unable to read the passphrase file.'); - } - - $stored = file($passpath, FILE_IGNORE_NEW_LINES); - - // "This message will self-destruct in five seconds." -- Mission Commander Swanbeck, Mission: Impossible II - unlink($passpath); - - if (is_readable($passpath)) { - throw new unauthorized_access_exception('Unable to remove the passphrase file.'); - } - - if (count($stored) < 2) { - throw new unauthorized_access_exception('Invalid format of the passphrase file.'); - } - - if (time() - (int)$stored[1] > 30 * 60) { - throw new unauthorized_access_exception('Passphrase timeout.'); - } - - if (strlen($stored[0]) < 24) { - throw new unauthorized_access_exception('Session passphrase not long enough.'); - } - - if ($password !== $stored[0]) { - throw new unauthorized_access_exception('Session passphrase does not match the stored one.'); - } - - $this->log('Successfully authorized using the passphrase file'); - } - - /** - * Returns the full path to the log file. - * - * @return string - */ - protected function log_location() { - if (!is_null($this->logfile)) { - return $this->logfile; - } - - $dataroot = $this->get_env('dataroot'); - - if (empty($dataroot)) { - $this->logfile = false; - return $this->logfile; - } - - $myroot = $dataroot.'/mdeploy'; - - if (!is_dir($myroot)) { - mkdir($myroot, 02777, true); - } - - $this->logfile = $myroot.'/mdeploy.log'; - return $this->logfile; - } - - /** - * Choose the target location for the given ZIP's URL. - * - * @param string $source URL - * @return string - */ - protected function target_location($source) { - $dataroot = $this->get_env('dataroot'); - $pool = $dataroot.'/mdeploy/var'; - - if (!is_dir($pool)) { - mkdir($pool, 02777, true); - } - - $target = $pool.'/'.md5($source); - - $suffix = 0; - while (file_exists($target.'.'.$suffix.'.zip')) { - $suffix++; - } - - return $target.'.'.$suffix.'.zip'; - } - - /** - * Choose the location of the current plugin folder backup - * - * @param string $path full path to the current folder - * @return string - */ - protected function backup_location($path) { - $dataroot = $this->get_env('dataroot'); - $pool = $dataroot.'/mdeploy/archive'; - - if (!is_dir($pool)) { - mkdir($pool, 02777, true); - } - - $target = $pool.'/'.basename($path).'_'.time(); - - $suffix = 0; - while (file_exists($target.'.'.$suffix)) { - $suffix++; - } - - return $target.'.'.$suffix; - } - - /** - * Downloads 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. - * - * @param string $source file url starting with http(s):// - * @param string $target store the downloaded content to this file (full path) - * @return bool true on success, false otherwise - * @throws download_file_exception - */ - protected function download_file($source, $target) { - - $newlines = array("\r", "\n"); - $source = str_replace($newlines, '', $source); - if (!preg_match('|^https?://|i', $source)) { - throw new download_file_exception('Unsupported transport protocol.'); - } - if (!$ch = curl_init($source)) { - $this->log('Unable to init cURL.'); - return false; - } - - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // verify the peer's certificate - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // check the existence of a common name and also verify that it matches the hostname provided - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the transfer as a string - curl_setopt($ch, CURLOPT_HEADER, false); // don't include the header in the output - curl_setopt($ch, CURLOPT_TIMEOUT, 3600); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // nah, moodle.org is never unavailable! :-p - curl_setopt($ch, CURLOPT_URL, $source); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Allow redirection, we trust in ssl. - curl_setopt($ch, CURLOPT_MAXREDIRS, 5); - - if ($cacertfile = $this->get_cacert()) { - // Do not use CA certs provided by the operating system. Instead, - // use this CA cert to verify the ZIP provider. - $this->log('Using custom CA certificate '.$cacertfile); - curl_setopt($ch, CURLOPT_CAINFO, $cacertfile); - } else { - $this->log('Using operating system CA certificates.'); - } - - $proxy = $this->input->get_option('proxy', false); - if (!empty($proxy)) { - curl_setopt($ch, CURLOPT_PROXY, $proxy); - - $proxytype = $this->input->get_option('proxytype', false); - if (strtoupper($proxytype) === 'SOCKS5') { - $this->log('Using SOCKS5 proxy'); - curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); - } else if (!empty($proxytype)) { - $this->log('Using HTTP proxy'); - curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); - curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false); - } - - $proxyuserpwd = $this->input->get_option('proxyuserpwd', false); - if (!empty($proxyuserpwd)) { - curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuserpwd); - curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM); - } - } - - $targetfile = fopen($target, 'w'); - - if (!$targetfile) { - throw new download_file_exception('Unable to create local file '.$target); - } - - curl_setopt($ch, CURLOPT_FILE, $targetfile); - - $result = curl_exec($ch); - - // try to detect encoding problems - if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) { - curl_setopt($ch, CURLOPT_ENCODING, 'none'); - $result = curl_exec($ch); - } - - fclose($targetfile); - - $this->curlerrno = curl_errno($ch); - $this->curlerror = curl_error($ch); - $this->curlinfo = curl_getinfo($ch); - - if (!$result or $this->curlerrno) { - $this->log('Curl Error.'); - return false; - - } else if (is_array($this->curlinfo) and (empty($this->curlinfo['http_code']) or ($this->curlinfo['http_code'] != 200))) { - $this->log('Curl remote error.'); - $this->log(print_r($this->curlinfo,true)); - return false; - } - - return true; - } - - /** - * Fetch environment settings. - * - * @param string $key The key to fetch - * @return mixed The value of the key if found. - * @throws invalid_setting_exception if the option is not set, or is invalid. - */ - protected function get_env($key) { - global $CFG; - - if (array_key_exists($key, $this->validconfigoptions)) { - if (isset($CFG->$key)) { - return $CFG->$key; - } - throw new invalid_setting_exception("Requested environment setting '{$key}' is not currently set."); - } else { - throw new invalid_setting_exception("Requested environment setting '{$key}' is invalid."); - } - } - - /** - * Get the location of ca certificates. - * @return string absolute file path or empty if default used - */ - protected function get_cacert() { - $dataroot = $this->get_env('dataroot'); - - // Bundle in dataroot always wins. - if (is_readable($dataroot.'/moodleorgca.crt')) { - return realpath($dataroot.'/moodleorgca.crt'); - } - - // Next comes the default from php.ini - $cacert = ini_get('curl.cainfo'); - if (!empty($cacert) and is_readable($cacert)) { - return realpath($cacert); - } - - // Windows PHP does not have any certs, we need to use something. - if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) { - if (is_readable(__DIR__.'/lib/cacert.pem')) { - return realpath(__DIR__.'/lib/cacert.pem'); - } - } - - // Use default, this should work fine on all properly configured *nix systems. - return null; - } - - /** - * Log a message - * - * @param string $message - */ - protected function log($message) { - - $logpath = $this->log_location(); - - if (empty($logpath)) { - // no logging available - return; - } - - $f = fopen($logpath, 'ab'); - - if ($f === false) { - throw new filesystem_exception('Unable to open the log file for appending'); - } - - $message = $this->format_log_message($message); - - fwrite($f, $message); - - fclose($f); - } - - /** - * Prepares the log message for writing into the file - * - * @param string $msg - * @return string - */ - protected function format_log_message($msg) { - - $msg = trim($msg); - $timestamp = date("Y-m-d H:i:s"); - - return $timestamp . ' '. $msg . PHP_EOL; - } - - /** - * Checks to see if the given source could be safely moved into a new location - * - * @param string $source full path to the existing directory - * @return bool - */ - protected function move_directory_source_precheck($source) { - - if (!is_writable($source)) { - return false; - } - - if (is_dir($source)) { - $handle = opendir($source); - } else { - return false; - } - - $result = true; - - while ($filename = readdir($handle)) { - $sourcepath = $source.'/'.$filename; - - if ($filename === '.' or $filename === '..') { - continue; - } - - if (is_dir($sourcepath)) { - $result = $result && $this->move_directory_source_precheck($sourcepath); - - } else { - $result = $result && is_writable($sourcepath); - } - } - - closedir($handle); - - return $result; - } - - /** - * 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) { - - // 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($path, 02777) && rmdir($path); - - return $result; - } - - /** - * Moves the given source into a new location recursively - * - * The target location can not exist. - * - * @param string $source full path to the existing directory - * @param string $destination full path to the new location of the folder - * @param bool $keepsourceroot should the root of the $source be kept or removed at the end - * @return bool - */ - protected function move_directory($source, $target, $keepsourceroot = false) { - - if (file_exists($target)) { - throw new filesystem_exception('Unable to move the directory - target location already exists'); - } - - return $this->move_directory_into($source, $target, $keepsourceroot); - } - - /** - * Moves the given source into a new location recursively - * - * If the target already exists, files are moved into it. The target is created otherwise. - * - * @param string $source full path to the existing directory - * @param string $destination full path to the new location of the folder - * @param bool $keepsourceroot should the root of the $source be kept or removed at the end - * @return bool - */ - protected function move_directory_into($source, $target, $keepsourceroot = false) { - - if (is_dir($source)) { - $handle = opendir($source); - } else { - throw new filesystem_exception('Source location is not a directory'); - } - - if (is_dir($target)) { - $result = true; - } else { - $result = mkdir($target, 02777); - } - - while ($filename = readdir($handle)) { - $sourcepath = $source.'/'.$filename; - $targetpath = $target.'/'.$filename; - - if ($filename === '.' or $filename === '..') { - continue; - } - - if (is_dir($sourcepath)) { - $result = $result && $this->move_directory($sourcepath, $targetpath, false); - - } else { - $result = $result && rename($sourcepath, $targetpath); - } - } - - closedir($handle); - - if (!$keepsourceroot) { - $result = $result && rmdir($source); - } - - clearstatcache(); - - return $result; - } - - /** - * Deletes the given directory recursively - * - * @param string $path full path to the directory - * @param bool $keeppathroot should the root of the $path be kept (i.e. remove the content only) or removed too - * @return bool - */ - protected function remove_directory($path, $keeppathroot = false) { - - $result = true; - - if (!file_exists($path)) { - return $result; - } - - if (is_dir($path)) { - $handle = opendir($path); - } else { - throw new filesystem_exception('Given path is not a directory'); - } - - while ($filename = readdir($handle)) { - $filepath = $path.'/'.$filename; - - if ($filename === '.' or $filename === '..') { - continue; - } - - if (is_dir($filepath)) { - $result = $result && $this->remove_directory($filepath, false); - - } else { - $result = $result && unlink($filepath); - } - } - - closedir($handle); - - if (!$keeppathroot) { - $result = $result && rmdir($path); - } - - clearstatcache(); - - return $result; - } - - /** - * Unzip the file obtained from the Plugins directory to this site - * - * @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|bool $backuplocation location of the previous version of the plugin or false for no backup - */ - protected function unzip_plugin($ziplocation, $plugintyperoot, $expectedlocation, $backuplocation) { - - $zip = new ZipArchive(); - $result = $zip->open($ziplocation); - - if ($result !== true) { - if ($backuplocation !== false) { - $this->move_directory($backuplocation, $expectedlocation); - } - throw new zip_exception('Unable to open the zip package'); - } - - // Make sure that the ZIP has expected structure - $pluginname = basename($expectedlocation); - for ($i = 0; $i < $zip->numFiles; $i++) { - $stat = $zip->statIndex($i); - $filename = $stat['name']; - $filename = explode('/', $filename); - if ($filename[0] !== $pluginname) { - $zip->close(); - throw new zip_exception('Invalid structure of the zip package'); - } - } - - if (!$zip->extractTo($plugintyperoot)) { - $zip->close(); - $this->remove_directory($expectedlocation, true); // just in case something was created - if ($backuplocation !== false) { - $this->move_directory_into($backuplocation, $expectedlocation); - } - throw new zip_exception('Unable to extract the zip package'); - } - - $zip->close(); - unlink($ziplocation); - } - - /** - * Redirect the browser - * - * @todo check if there has been some output yet - * @param string $url - */ - protected function redirect($url) { - header('Location: '.$url); - } -} - - -/** - * Provides exception handlers for this script - */ -class exception_handlers { - - /** - * Sets the exception handler - * - * - * @param string $handler name - */ - public static function set_handler($handler) { - - if (PHP_SAPI === 'cli') { - // No custom handler available for CLI mode. - set_exception_handler(null); - return; - } - - set_exception_handler('exception_handlers::'.$handler.'_exception_handler'); - } - - /** - * Returns the text describing the thrown exception - * - * By default, PHP displays full path to scripts when the exception is thrown. In order to prevent - * sensitive information leak (and yes, the path to scripts at a web server _is_ sensitive information) - * the path to scripts is removed from the message. - * - * @param Exception $e thrown exception - * @return string - */ - public static function format_exception_info(Exception $e) { - - $mydir = dirname(__FILE__).'/'; - $text = $e->__toString(); - $text = str_replace($mydir, '', $text); - return $text; - } - - /** - * Very basic exception handler - * - * @param Exception $e uncaught exception - */ - public static function bootstrap_exception_handler(Exception $e) { - echo('

Oops! It did it again

'); - echo('

Moodle deployment utility had a trouble with your request. See the debugging information for more details.

'); - echo('
');
-        echo self::format_exception_info($e);
-        echo('
'); - } - - /** - * Default exception handler - * - * When this handler is used, input_manager and output_manager singleton instances already - * exist in the memory and can be used. - * - * @param Exception $e uncaught exception - */ - public static function default_exception_handler(Exception $e) { - - $worker = worker::instance(); - $worker->log_exception($e); - - $output = output_manager::instance(); - $output->exception($e); - } -} - -//////////////////////////////////////////////////////////////////////////////// - -// Check if the script is actually executed or if it was just included by someone -// else - typically by the PHPUnit. This is a PHP alternative to the Python's -// if __name__ == '__main__' -if (!debug_backtrace()) { - // We are executed by the SAPI. - exception_handlers::set_handler('bootstrap'); - // Initialize the worker class to actually make the job. - $worker = worker::instance(); - exception_handlers::set_handler('default'); - - // Lights, Camera, Action! - $worker->execute(); - -} else { - // We are included - probably by some unit testing framework. Do nothing. -} diff --git a/mdeploytest.php b/mdeploytest.php deleted file mode 100644 index dcd2dfddde1..00000000000 --- a/mdeploytest.php +++ /dev/null @@ -1,370 +0,0 @@ -. - -/** - * PHPUnit tests for the mdeploy.php utility - * - * Because the mdeploy.php can't be part of the Moodle code itself, this tests must be - * executed using something like: - * - * $ phpunit --no-configuration mdeploytest - * - * @package core - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -require(__DIR__.'/mdeploy.php'); - -/** - * Provides testable input options. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class input_fake_provider extends input_provider { - - /** @var array */ - protected $fakeoptions = array(); - - /** - * Sets fake raw options. - * - * @param array $options - */ - public function set_fake_options(array $options) { - $this->fakeoptions = $options; - $this->populate_options(); - } - - /** - * Returns the explicitly set fake options. - * - * @return array - */ - protected function parse_raw_options() { - return $this->fakeoptions; - } -} - -/** - * Testable subclass. - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class testable_input_manager extends input_manager { - - /** - * Provides access to the protected method so we can test it explicitly. - */ - public function cast_value($raw, $type) { - return parent::cast_value($raw, $type); - } - - /** - * Sets the fake input provider. - */ - protected function initialize() { - $this->inputprovider = input_fake_provider::instance(); - } -} - - -/** - * Testable subclass - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class testable_worker extends worker { - - /** - * Provides access to the protected method. - */ - public function move_directory($source, $target, $keepsourceroot = false) { - return parent::move_directory($source, $target, $keepsourceroot); - } - - /** - * Provides access to the protected method. - */ - 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); - } - - public function get_env($key) { - return parent::get_env($key); - } -} - - -/** - * Test cases for the mdeploy utility - * - * @copyright 2012 David Mudrak - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class mdeploytest extends PHPUnit_Framework_TestCase { - - public function test_same_singletons() { - $a = input_manager::instance(); - $b = input_manager::instance(); - $this->assertSame($a, $b); - } - - /** - * @dataProvider data_for_cast_value - */ - public function test_cast_value($raw, $type, $result) { - $input = testable_input_manager::instance(); - $this->assertSame($input->cast_value($raw, $type), $result); - } - - public function data_for_cast_value() { - return array( - array('3', input_manager::TYPE_INT, 3), - array(4, input_manager::TYPE_INT, 4), - array('', input_manager::TYPE_INT, 0), - - array(true, input_manager::TYPE_FLAG, true), - array(false, input_manager::TYPE_FLAG, true), - array(0, input_manager::TYPE_FLAG, true), - array('1', input_manager::TYPE_FLAG, true), - array('0', input_manager::TYPE_FLAG, true), - array('muhehe', input_manager::TYPE_FLAG, true), - - array('C:\\WINDOWS\\user.dat', input_manager::TYPE_PATH, 'C:/WINDOWS/user.dat'), - array('D:\xampp\htdocs\24_integration/mdeploy.php', input_manager::TYPE_PATH, 'D:/xampp/htdocs/24_integration/mdeploy.php'), - array('d:/xampp/htdocs/24_integration/mdeploy.php', input_manager::TYPE_PATH, 'd:/xampp/htdocs/24_integration/mdeploy.php'), - array('../../../etc/passwd', input_manager::TYPE_PATH, '/etc/passwd'), - array('///////.././public_html/test.php', input_manager::TYPE_PATH, '/public_html/test.php'), - - array("!@#$%|/etc/qwerty\n\n\t\n\r", input_manager::TYPE_RAW, "!@#$%|/etc/qwerty\n\n\t\n\r"), - - array("\nrock'n'roll.mp3\t.exe", input_manager::TYPE_FILE, 'rocknroll.mp3.exe'), - - array('http://localhost/moodle/dev/plugin.zip', input_manager::TYPE_URL, 'http://localhost/moodle/dev/plugin.zip'), - array( - 'https://moodle.org/plugins/download.php/1292/mod_stampcoll_moodle23_2012062201.zip', - input_manager::TYPE_URL, - 'https://moodle.org/plugins/download.php/1292/mod_stampcoll_moodle23_2012062201.zip' - ), - - array('5e8d2ea4f50d154730100b1645fbad67', input_manager::TYPE_MD5, '5e8d2ea4f50d154730100b1645fbad67'), - ); - } - - /** - * @expectedException invalid_option_exception - */ - public function test_input_type_path_multiple_colons() { - $input = testable_input_manager::instance(); - $input->cast_value('C:\apache\log:file', input_manager::TYPE_PATH); // must throw exception - } - - /** - * @expectedException invalid_option_exception - */ - public function test_input_type_path_invalid_drive_label() { - $input = testable_input_manager::instance(); - $input->cast_value('0:/srv/moodledata', input_manager::TYPE_PATH); // must throw exception - } - - /** - * @expectedException invalid_option_exception - */ - public function test_input_type_path_invalid_colon() { - $input = testable_input_manager::instance(); - $input->cast_value('/var/www/moodle:2.5', input_manager::TYPE_PATH); // must throw exception - } - - /** - * @expectedException invalid_coding_exception - */ - public function test_cast_array_argument() { - $input = testable_input_manager::instance(); - $input->cast_value(array(1, 2, 3), input_manager::TYPE_INT); // must throw exception - } - - /** - * @expectedException invalid_coding_exception - */ - public function test_cast_object_argument() { - $input = testable_input_manager::instance(); - $o = new stdClass(); - $input->cast_value($o, input_manager::TYPE_INT); // must throw exception - } - - /** - * @expectedException invalid_option_exception - */ - public function test_cast_invalid_url_value() { - $input = testable_input_manager::instance(); - $invalid = 'file:///etc/passwd'; - $input->cast_value($invalid, input_manager::TYPE_URL); // must throw exception - } - - /** - * @expectedException invalid_option_exception - */ - public function test_cast_invalid_md5_value() { - $input = testable_input_manager::instance(); - $invalid = 'this is not a valid md5 hash'; - $input->cast_value($invalid, input_manager::TYPE_MD5); // must throw exception - } - - /** - * @expectedException invalid_option_exception - */ - public function test_cast_tilde_in_path() { - $input = testable_input_manager::instance(); - $invalid = '~/public_html/moodle_dev'; - $input->cast_value($invalid, input_manager::TYPE_PATH); // must throw exception - } - - public function test_has_option() { - $provider = input_fake_provider::instance(); - - $provider->set_fake_options(array()); - $this->assertFalse($provider->has_option('foo')); // foo not passed - - $provider->set_fake_options(array('foo' => 1)); - $this->assertFalse($provider->has_option('foo')); // foo passed but not a known option - - $provider->set_fake_options(array('foo' => 1, 'help' => false)); - $this->assertTrue($provider->has_option('help')); // help passed and it is a flag (value ignored) - $this->assertTrue($provider->has_option('h')); // 'h' is a shortname for 'help' - } - - public function test_get_option() { - $input = testable_input_manager::instance(); - $provider = input_fake_provider::instance(); - - $provider->set_fake_options(array('help' => false, 'passfile' => '_mdeploy.123456')); - $this->assertTrue($input->get_option('h')); - $this->assertEquals($input->get_option('passfile'), '_mdeploy.123456'); - $this->assertEquals($input->get_option('password', 'admin123'), 'admin123'); - try { - $this->assertEquals($input->get_option('password'), 'admin123'); // must throw exception (not passed but required) - $this->assertTrue(false); - } catch (missing_option_exception $e) { - $this->assertTrue(true); - } - } - - public function test_moving_and_removing_directories() { - $worker = testable_worker::instance(); - - $root = sys_get_temp_dir().'/'.uniqid('mdeploytest', true); - mkdir($root.'/a', 0777, true); - touch($root.'/a/a.txt'); - - $this->assertTrue(file_exists($root.'/a/a.txt')); - $this->assertFalse(file_exists($root.'/b/a.txt')); - $this->assertTrue($worker->move_directory($root.'/a', $root.'/b')); - $this->assertFalse(is_dir($root.'/a')); - $this->assertTrue(file_exists($root.'/b/a.txt')); - $this->assertTrue($worker->move_directory($root.'/b', $root.'/c', true)); - $this->assertTrue(file_exists($root.'/c/a.txt')); - $this->assertFalse(file_exists($root.'/b/a.txt')); - $this->assertTrue(is_dir($root.'/b')); - $this->assertTrue($worker->remove_directory($root.'/c', true)); - $this->assertFalse(file_exists($root.'/c/a.txt')); - $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. - } - - /** - * Test that an invalid setting throws an exception. - * - * @dataProvider get_env_unlisted_provider - * @expectedException invalid_setting_exception - * @expectedExceptionMessageRegExp /^Requested environment setting '[^']*' is invalid.$/ - */ - public function test_get_env_unlisted($key) { - $worker = testable_worker::instance(); - $worker->get_env($key); - } - - public function get_env_unlisted_provider() { - return array( - // Completely invalid environment variables. - array('example'), - array('invalid'), - - // Valid ones which have not been whitelisted. - array('noemailever'), - array('dbname'), - ); - } - - /** - * Test that a valid, but unset setting throws an exception. - * - * @dataProvider get_env_valid_provider - * @expectedException invalid_setting_exception - * @expectedExceptionMessageRegExp /^Requested environment setting '[^']*' is not current set.$/ - */ - public function test_get_env_unset($key) { - // Ensure that the setting is currently unset. - global $CFG; - $CFG->$key = null; - - $worker = testable_worker::instance(); - $worker->get_env($key); - } - - /** - * Test that a valid setting with data returns that data. - * - * @dataProvider get_env_valid_provider - */ - public function test_get_env_valid($key) { - // Ensure that the setting is currently unset. - global $CFG; - $CFG->$key = rand(0, 1000); - - $worker = testable_worker::instance(); - $value = $worker->get_env($key); - - $this->assertEquals($CFG->$key, $value); - } - - public function get_env_valid_provider() { - return array( - array('dataroot'), - array('dirroot'), - ); - } -}