Commit bdfb778a authored by webchick's avatar webchick

Issue #2042447 by David_Rothstein, joelpittet, stefan.r, SebCorbin, mpdonadio,...

Issue #2042447 by David_Rothstein, joelpittet, stefan.r, SebCorbin, mpdonadio, steinmb, webchick, Cottser, miniwebs2, Vj, Gilbert Rehling, nuwe, yched, StuartJNCC, Fabianx, yktdan, herom: Install a module user interface does not install modules (or themes)
parent d830e59b
...@@ -139,7 +139,13 @@ function authorize_access_allowed(Request $request) { ...@@ -139,7 +139,13 @@ function authorize_access_allowed(Request $request) {
} }
// If a batch is running, let it run. // If a batch is running, let it run.
elseif ($request->query->has('batch')) { elseif ($request->query->has('batch')) {
$content = ['#markup' => _batch_page($request)]; $content = _batch_page($request);
// If _batch_page() returns a response object (likely a JsonResponse for
// JavaScript-based batch processing), send it immediately.
if ($content instanceof Response) {
$content->send();
exit;
}
} }
else { else {
if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) { if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) {
......
...@@ -124,7 +124,10 @@ function theme_authorize_report($variables) { ...@@ -124,7 +124,10 @@ function theme_authorize_report($variables) {
'#message' => $log_message['message'], '#message' => $log_message['message'],
'#success' => $log_message['success'], '#success' => $log_message['success'],
); );
$items[] = drupal_render($authorize_message); $items[] = array(
'#markup' => drupal_render($authorize_message),
'#wrapper_attributes' => array('class' => $log_message['success'] ? 'authorize-results__success' : 'authorize-results__failure'),
);
} }
$item_list = array( $item_list = array(
'#theme' => 'item_list', '#theme' => 'item_list',
...@@ -151,13 +154,6 @@ function theme_authorize_report($variables) { ...@@ -151,13 +154,6 @@ function theme_authorize_report($variables) {
* @ingroup themeable * @ingroup themeable
*/ */
function theme_authorize_message($variables) { function theme_authorize_message($variables) {
$message = $variables['message']; $item = array('#markup' => $variables['message']);
$success = $variables['success']; return drupal_render($item);
if ($success) {
$item = array('data' => array('#markup' => $message), 'class' => array('authorize-results__success'));
}
else {
$item = array('data' => array('#markup' => $message), 'class' => array('authorize-results__failure'));
}
return $item;
} }
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element; use Drupal\Core\Render\Element;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/** /**
* Provides the file transfer authorization form. * Provides the file transfer authorization form.
...@@ -226,7 +227,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -226,7 +227,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$filetransfer = $this->getFiletransfer($filetransfer_backend, $form_connection_settings[$filetransfer_backend]); $filetransfer = $this->getFiletransfer($filetransfer_backend, $form_connection_settings[$filetransfer_backend]);
// Now run the operation. // Now run the operation.
$this->runOperation($filetransfer); $response = $this->runOperation($filetransfer);
if ($response instanceof Response) {
$form_state->setResponse($response);
}
} }
catch (\Exception $e) { catch (\Exception $e) {
// If there is no database available, we don't care and just skip // If there is no database available, we don't care and just skip
...@@ -333,13 +337,18 @@ protected function setConnectionSettingsDefaults(&$element, $key, array $default ...@@ -333,13 +337,18 @@ protected function setConnectionSettingsDefaults(&$element, $key, array $default
* *
* @param $filetransfer * @param $filetransfer
* The FileTransfer object to use for running the operation. * The FileTransfer object to use for running the operation.
*
* @return \Symfony\Component\HttpFoundation\Response|null
* The result of running the operation. If this is an instance of
* \Symfony\Component\HttpFoundation\Response the calling code should use
* that response for the current page request.
*/ */
protected function runOperation($filetransfer) { protected function runOperation($filetransfer) {
$operation = $_SESSION['authorize_operation']; $operation = $_SESSION['authorize_operation'];
unset($_SESSION['authorize_operation']); unset($_SESSION['authorize_operation']);
require_once $this->root . '/' . $operation['file']; require_once $this->root . '/' . $operation['file'];
call_user_func_array($operation['callback'], array_merge(array($filetransfer), $operation['arguments'])); return call_user_func_array($operation['callback'], array_merge(array($filetransfer), $operation['arguments']));
} }
} }
...@@ -20,7 +20,7 @@ class Module extends Updater implements UpdaterInterface { ...@@ -20,7 +20,7 @@ class Module extends Updater implements UpdaterInterface {
* *
* If the module is already installed, drupal_get_path() will return * If the module is already installed, drupal_get_path() will return
* a valid path and we should install it there (although we need to use an * a valid path and we should install it there (although we need to use an
* absolute path, so we prepend DRUPAL_ROOT). If we're installing a new * absolute path, so we prepend the root path). If we're installing a new
* module, we always want it to go into /modules, since that's * module, we always want it to go into /modules, since that's
* where all the documentation recommends users install their modules, and * where all the documentation recommends users install their modules, and
* there's no way that can conflict on a multi-site installation, since * there's no way that can conflict on a multi-site installation, since
...@@ -31,20 +31,30 @@ class Module extends Updater implements UpdaterInterface { ...@@ -31,20 +31,30 @@ class Module extends Updater implements UpdaterInterface {
* A directory path. * A directory path.
*/ */
public function getInstallDirectory() { public function getInstallDirectory() {
if ($relative_path = drupal_get_path('module', $this->name)) { if ($this->isInstalled() && ($relative_path = drupal_get_path('module', $this->name))) {
$relative_path = dirname($relative_path); $relative_path = dirname($relative_path);
} }
else { else {
$relative_path = 'modules'; $relative_path = $this->getRootDirectoryRelativePath();
} }
return DRUPAL_ROOT . '/' . $relative_path; return $this->root . '/' . $relative_path;
}
/**
* {@inheritdoc}
*/
public static function getRootDirectoryRelativePath() {
return 'modules';
} }
/** /**
* Implements Drupal\Core\Updater\UpdaterInterface::isInstalled(). * Implements Drupal\Core\Updater\UpdaterInterface::isInstalled().
*/ */
public function isInstalled() { public function isInstalled() {
return (bool) drupal_get_path('module', $this->name); // Check if the module exists in the file system, regardless of whether it
// is enabled or not.
$modules = \Drupal::state()->get('system.module.files', array());
return isset($modules[$this->name]);
} }
/** /**
......
...@@ -20,7 +20,7 @@ class Theme extends Updater implements UpdaterInterface { ...@@ -20,7 +20,7 @@ class Theme extends Updater implements UpdaterInterface {
* *
* If the theme is already installed, drupal_get_path() will return * If the theme is already installed, drupal_get_path() will return
* a valid path and we should install it there (although we need to use an * a valid path and we should install it there (although we need to use an
* absolute path, so we prepend DRUPAL_ROOT). If we're installing a new * absolute path, so we prepend the root path). If we're installing a new
* theme, we always want it to go into /themes, since that's * theme, we always want it to go into /themes, since that's
* where all the documentation recommends users install their themes, and * where all the documentation recommends users install their themes, and
* there's no way that can conflict on a multi-site installation, since * there's no way that can conflict on a multi-site installation, since
...@@ -31,20 +31,30 @@ class Theme extends Updater implements UpdaterInterface { ...@@ -31,20 +31,30 @@ class Theme extends Updater implements UpdaterInterface {
* A directory path. * A directory path.
*/ */
public function getInstallDirectory() { public function getInstallDirectory() {
if ($relative_path = drupal_get_path('theme', $this->name)) { if ($this->isInstalled() && ($relative_path = drupal_get_path('theme', $this->name))) {
$relative_path = dirname($relative_path); $relative_path = dirname($relative_path);
} }
else { else {
$relative_path = 'themes'; $relative_path = $this->getRootDirectoryRelativePath();
} }
return DRUPAL_ROOT . '/' . $relative_path; return $this->root . '/' . $relative_path;
}
/**
* {@inheritdoc}
*/
public static function getRootDirectoryRelativePath() {
return 'themes';
} }
/** /**
* Implements Drupal\Core\Updater\UpdaterInterface::isInstalled(). * Implements Drupal\Core\Updater\UpdaterInterface::isInstalled().
*/ */
public function isInstalled() { public function isInstalled() {
return (bool) drupal_get_path('theme', $this->name); // Check if the theme exists in the file system, regardless of whether it
// is enabled or not.
$themes = \Drupal::state()->get('system.theme.files', array());
return isset($themes[$this->name]);
} }
/** /**
......
...@@ -23,14 +23,26 @@ class Updater { ...@@ -23,14 +23,26 @@ class Updater {
*/ */
public $source; public $source;
/**
* The root directory under which new projects will be copied.
*
* @var string
*/
protected $root;
/** /**
* Constructs a new updater. * Constructs a new updater.
* *
* @param string $source * @param string $source
* Directory to install from. * Directory to install from.
* @param string $root
* The root directory under which the project will be copied to if it's a
* new project. Usually this is the app root (the directory in which the
* Drupal site is installed).
*/ */
public function __construct($source) { public function __construct($source, $root) {
$this->source = $source; $this->source = $source;
$this->root = $root;
$this->name = self::getProjectName($source); $this->name = self::getProjectName($source);
$this->title = self::getProjectTitle($source); $this->title = self::getProjectTitle($source);
} }
...@@ -43,20 +55,24 @@ public function __construct($source) { ...@@ -43,20 +55,24 @@ public function __construct($source) {
* *
* @param string $source * @param string $source
* Directory of a Drupal project. * Directory of a Drupal project.
* @param string $root
* The root directory under which the project will be copied to if it's a
* new project. Usually this is the app root (the directory in which the
* Drupal site is installed).
* *
* @return \Drupal\Core\Updater\Updater * @return \Drupal\Core\Updater\Updater
* A new Drupal\Core\Updater\Updater object. * A new Drupal\Core\Updater\Updater object.
* *
* @throws \Drupal\Core\Updater\UpdaterException * @throws \Drupal\Core\Updater\UpdaterException
*/ */
public static function factory($source) { public static function factory($source, $root) {
if (is_dir($source)) { if (is_dir($source)) {
$updater = self::getUpdaterFromDirectory($source); $updater = self::getUpdaterFromDirectory($source);
} }
else { else {
throw new UpdaterException(t('Unable to determine the type of the source directory.')); throw new UpdaterException(t('Unable to determine the type of the source directory.'));
} }
return new $updater($source); return new $updater($source, $root);
} }
/** /**
......
...@@ -35,13 +35,21 @@ public function isInstalled(); ...@@ -35,13 +35,21 @@ public function isInstalled();
public static function getProjectName($directory); public static function getProjectName($directory);
/** /**
* Returns the path to the default install location. * Returns the path to the default install location for the current project.
* *
* @return string * @return string
* An absolute path to the default install location. * An absolute path to the default install location.
*/ */
public function getInstallDirectory(); public function getInstallDirectory();
/**
* Returns the name of the root directory under which projects will be copied.
*
* @return string
* A relative path to the root directory.
*/
public static function getRootDirectoryRelativePath();
/** /**
* Determines if the Updater can handle the project provided in $directory. * Determines if the Updater can handle the project provided in $directory.
* *
......
...@@ -460,7 +460,7 @@ function system_authorized_run($callback, $file, $arguments = array(), $page_tit ...@@ -460,7 +460,7 @@ function system_authorized_run($callback, $file, $arguments = array(), $page_tit
function system_authorized_batch_process() { function system_authorized_batch_process() {
$finish_url = system_authorized_get_url(); $finish_url = system_authorized_get_url();
$process_url = system_authorized_batch_processing_url(); $process_url = system_authorized_batch_processing_url();
return batch_process($finish_url->toString(), $process_url); return batch_process($finish_url->setAbsolute()->toString(), $process_url);
} }
/** /**
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Updater\Updater; use Drupal\Core\Updater\Updater;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/** /**
* Configure update settings for this site. * Configure update settings for this site.
...@@ -27,7 +28,7 @@ class UpdateManagerInstall extends FormBase { ...@@ -27,7 +28,7 @@ class UpdateManagerInstall extends FormBase {
protected $moduleHandler; protected $moduleHandler;
/** /**
* The app root. * The root location under which installed projects will be saved.
* *
* @var string * @var string
*/ */
...@@ -44,7 +45,7 @@ class UpdateManagerInstall extends FormBase { ...@@ -44,7 +45,7 @@ class UpdateManagerInstall extends FormBase {
* Constructs a new UpdateManagerInstall. * Constructs a new UpdateManagerInstall.
* *
* @param string $root * @param string $root
* The app root. * The root location under which installed projects will be saved.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler. * The module handler.
* @param string $site_path * @param string $site_path
...@@ -68,7 +69,7 @@ public function getFormId() { ...@@ -68,7 +69,7 @@ public function getFormId() {
*/ */
public static function create(ContainerInterface $container) { public static function create(ContainerInterface $container) {
return new static( return new static(
$container->get('app.root'), $container->get('update.root'),
$container->get('module_handler'), $container->get('module_handler'),
$container->get('site.path') $container->get('site.path')
); );
...@@ -192,7 +193,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -192,7 +193,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$project_location = $directory . '/' . $project; $project_location = $directory . '/' . $project;
try { try {
$updater = Updater::factory($project_location); $updater = Updater::factory($project_location, $this->root);
} }
catch (\Exception $e) { catch (\Exception $e) {
drupal_set_message($e->getMessage(), 'error'); drupal_set_message($e->getMessage(), 'error');
...@@ -231,7 +232,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -231,7 +232,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if (fileowner($project_real_location) == fileowner($this->sitePath)) { if (fileowner($project_real_location) == fileowner($this->sitePath)) {
$this->moduleHandler->loadInclude('update', 'inc', 'update.authorize'); $this->moduleHandler->loadInclude('update', 'inc', 'update.authorize');
$filetransfer = new Local($this->root); $filetransfer = new Local($this->root);
call_user_func_array('update_authorize_run_install', array_merge(array($filetransfer), $arguments)); $response = call_user_func_array('update_authorize_run_install', array_merge(array($filetransfer), $arguments));
if ($response instanceof Response) {
$form_state->setResponse($response);
}
} }
// Otherwise, go through the regular workflow to prompt for FTP/SSH // Otherwise, go through the regular workflow to prompt for FTP/SSH
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
use Drupal\Core\State\StateInterface; use Drupal\Core\State\StateInterface;
use Drupal\Core\Updater\Updater; use Drupal\Core\Updater\Updater;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/** /**
* Configure update settings for this site. * Configure update settings for this site.
...@@ -21,7 +22,7 @@ ...@@ -21,7 +22,7 @@
class UpdateReady extends FormBase { class UpdateReady extends FormBase {
/** /**
* The app root. * The root location under which updated projects will be saved.
* *
* @var string * @var string
*/ */
...@@ -52,7 +53,7 @@ class UpdateReady extends FormBase { ...@@ -52,7 +53,7 @@ class UpdateReady extends FormBase {
* Constructs a new UpdateReady object. * Constructs a new UpdateReady object.
* *
* @param string $root * @param string $root
* The app root. * The root location under which updated projects will be saved.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The object that manages enabled modules in a Drupal installation. * The object that manages enabled modules in a Drupal installation.
* @param \Drupal\Core\State\StateInterface $state * @param \Drupal\Core\State\StateInterface $state
...@@ -79,7 +80,7 @@ public function getFormId() { ...@@ -79,7 +80,7 @@ public function getFormId() {
*/ */
public static function create(ContainerInterface $container) { public static function create(ContainerInterface $container) {
return new static( return new static(
$container->get('app.root'), $container->get('update.root'),
$container->get('module_handler'), $container->get('module_handler'),
$container->get('state'), $container->get('state'),
$container->get('site.path') $container->get('site.path')
...@@ -139,7 +140,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -139,7 +140,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$project_real_location = NULL; $project_real_location = NULL;
foreach ($projects as $project => $url) { foreach ($projects as $project => $url) {
$project_location = $directory . '/' . $project; $project_location = $directory . '/' . $project;
$updater = Updater::factory($project_location); $updater = Updater::factory($project_location, $this->root);
$project_real_location = drupal_realpath($project_location); $project_real_location = drupal_realpath($project_location);
$updates[] = array( $updates[] = array(
'project' => $project, 'project' => $project,
...@@ -156,7 +157,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -156,7 +157,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if (fileowner($project_real_location) == fileowner($this->sitePath)) { if (fileowner($project_real_location) == fileowner($this->sitePath)) {
$this->moduleHandler->loadInclude('update', 'inc', 'update.authorize'); $this->moduleHandler->loadInclude('update', 'inc', 'update.authorize');
$filetransfer = new Local($this->root); $filetransfer = new Local($this->root);
update_authorize_run_update($filetransfer, $updates); $response = update_authorize_run_update($filetransfer, $updates);
if ($response instanceof Response) {
$form_state->setResponse($response);
}
} }
// Otherwise, go through the regular workflow to prompt for FTP/SSH // Otherwise, go through the regular workflow to prompt for FTP/SSH
// credentials and invoke update_authorize_run_update() indirectly with // credentials and invoke update_authorize_run_update() indirectly with
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
namespace Drupal\update\Tests; namespace Drupal\update\Tests;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase; use Drupal\simpletest\WebTestBase;
...@@ -29,6 +30,29 @@ ...@@ -29,6 +30,29 @@
*/ */
abstract class UpdateTestBase extends WebTestBase { abstract class UpdateTestBase extends WebTestBase {
protected function setUp() {
parent::setUp();
// Change the root path which Update Manager uses to install and update
// projects to be inside the testing site directory. See
// \Drupal\updateUpdateRootFactory::get() for equivalent changes to the
// test child site.
$request = \Drupal::request();
$update_root = $this->container->get('update.root') . '/' . DrupalKernel::findSitePath($request);
$this->container->set('update.root', $update_root);
\Drupal::setContainer($this->container);
// Create the directories within the root path within which the Update
// Manager will install projects.
foreach (drupal_get_updaters() as $updater_info) {
$updater = $updater_info['class'];
$install_directory = $update_root . '/' . $updater::getRootDirectoryRelativePath();
if (!is_dir($install_directory)) {
mkdir($install_directory);
}
}
}
/** /**
* Refreshes the update status based on the desired available update scenario. * Refreshes the update status based on the desired available update scenario.
* *
......
...@@ -46,6 +46,7 @@ public function testUploadModule() { ...@@ -46,6 +46,7 @@ public function testUploadModule() {
// This also checks that the correct archive extensions are allowed. // This also checks that the correct archive extensions are allowed.
$this->drupalPostForm('admin/modules/install', $edit, t('Install')); $this->drupalPostForm('admin/modules/install', $edit, t('Install'));
$this->assertText(t('Only files with the following extensions are allowed: @archive_extensions.', array('@archive_extensions' => archiver_get_extensions())),'Only valid archives can be uploaded.'); $this->assertText(t('Only files with the following extensions are allowed: @archive_extensions.', array('@archive_extensions' => archiver_get_extensions())),'Only valid archives can be uploaded.');
$this->assertUrl('admin/modules/install');
// Check to ensure an existing module can't be reinstalled. Also checks that // Check to ensure an existing module can't be reinstalled. Also checks that
// the archive was extracted since we can't know if the module is already // the archive was extracted since we can't know if the module is already
...@@ -56,6 +57,24 @@ public function testUploadModule() { ...@@ -56,6 +57,24 @@ public function testUploadModule() {
); );
$this->drupalPostForm('admin/modules/install', $edit, t('Install')); $this->drupalPostForm('admin/modules/install', $edit, t('Install'));
$this->assertText(t('@module_name is already installed.', array('@module_name' => 'AAA Update test')), 'Existing module was extracted and not reinstalled.'); $this->assertText(t('@module_name is already installed.', array('@module_name' => 'AAA Update test')), 'Existing module was extracted and not reinstalled.');
$this->assertUrl('admin/modules/install');
// Ensure that a new module can be extracted and installed.
$updaters = drupal_get_updaters();
$moduleUpdater = $updaters['module']['class'];
$installedInfoFilePath = $this->container->get('update.root') . '/' . $moduleUpdater::getRootDirectoryRelativePath() . '/update_test_new_module/update_test_new_module.info.yml';
$this->assertFalse(file_exists($installedInfoFilePath), 'The new module does not exist in the filesystem before it is installed with the Update Manager.');
$validArchiveFile = drupal_get_path('module', 'update') . '/tests/update_test_new_module.tar.gz';
$edit = array(
'files[project_upload]' => $validArchiveFile,
);
$this->drupalPostForm('admin/modules/install', $edit, t('Install'));
// Check that submitting the form takes the user to authorize.php.
$this->assertUrl('core/authorize.php');
// Check for a success message on the page, and check that the installed
// module now exists in the expected place in the filesystem.
$this->assertRaw(t('Installed %project_name successfully', array('%project_name' => 'update_test_new_module')));
$this->assertTrue(file_exists($installedInfoFilePath), 'The new module exists in the filesystem after it is installed with the Update Manager.');
} }
/** /**
......
<?php
/**
* @file
* Contains \Drupal\update\UpdateRootFactory.
*/
namespace Drupal\update;
use Drupal\Core\DrupalKernelInterface;
use Symfony\Component\HttpFoundation\RequestStack;