diff --git a/admin/renderer.php b/admin/renderer.php index 581cdd112c9..7ca6ea24dbd 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -1206,9 +1206,14 @@ class core_admin_renderer extends plugin_renderer_base { $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), ''); $deployer = available_update_deployer::instance(); - if ($deployer->initialized() and $deployer->can_deploy($updateinfo)) { - $widget = $deployer->make_confirm_widget($updateinfo); - $box .= $this->output->render($widget); + 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')); + } } $box .= $this->output->box_end(); diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 6812824c87f..f21667ff020 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -41,6 +41,10 @@ $string['nonehighlighted'] = 'No plugins require your attention now'; $string['nonehighlightedinfo'] = 'Display the list of all installed plugins anyway'; $string['noneinstalled'] = 'No plugins of this type are installed'; $string['notes'] = 'Notes'; +$string['notwritable'] = 'Plugin files not writable'; +$string['notwritable_help'] = 'You have enabled automatic updates deployment and there is available update for this plugin. However, the plugin files are not writable by the web server so the update can not be installed at the moment. + +Make the plugin folder and all its contents writable to be able to install the available update automatically.'; $string['numtotal'] = 'Installed: {$a}'; $string['numdisabled'] = 'Disabled: {$a}'; $string['numextension'] = 'Contributions: {$a}'; diff --git a/lib/pluginlib.php b/lib/pluginlib.php index bd7f6d47c44..e8cb001ce7a 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -1538,26 +1538,32 @@ class available_update_deployer { } /** - * Check if the available update info contains all required data for deployment. + * Returns a list of reasons why the deployment can not happen * - * All instances of {@link available_update_info} class always provide at least the - * component name and component version. Additionally, we also need the URL to download - * the ZIP package from and MD5 hash of the ZIP's content. + * 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 available_update_info $info - * @return bool + * @return array */ - public function can_deploy(available_update_info $info) { + public function deployment_impediments(available_update_info $info) { + + $impediments = array(); if (empty($info->download)) { - return false; + $impediments['missingdownloadurl'] = true; } if (empty($info->downloadmd5)) { - return false; + $impediments['missingdownloadmd5'] = true; } - return true; + if (!$this->component_writable($info->component)) { + $impediments['notwritable'] = true; + } + + return $impediments; } /** @@ -1783,7 +1789,6 @@ class available_update_deployer { } } - // End of external API /** @@ -1857,6 +1862,70 @@ class available_update_deployer { 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) = normalize_component($component); + + $directory = get_plugin_directory($plugintype, $pluginname); + + if (is_null($directory)) { + throw new coding_exception('Unknown component location', $component); + } + + return $this->directory_writable($directory); + } + + /** + * 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/mdeploy.php b/mdeploy.php index 8e5bfec9da2..9fba3bad2b4 100644 --- a/mdeploy.php +++ b/mdeploy.php @@ -1048,6 +1048,10 @@ class worker extends singleton_pattern { */ protected function move_directory_source_precheck($source) { + if (!is_writable($source)) { + return false; + } + if (is_dir($source)) { $handle = opendir($source); } else { @@ -1072,7 +1076,8 @@ class worker extends singleton_pattern { } closedir($handle); - return $result && is_writable($source); + + return $result; } /**