diff --git a/core/authorize.php b/core/authorize.php index d9f1e56a8d851c33d3fbf5f9c3376345d9f58a71..1138c92b89ee63e7f8e49961556425515b82202b 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -139,7 +139,13 @@ function authorize_access_allowed(Request $request) { } // If a batch is running, let it run. 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 { if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) { diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 16779afd1962a1b6f90b904016e0ac4fddf77992..d23addb73fc11adfa37e90d75430cee3427713dc 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -124,7 +124,10 @@ function theme_authorize_report($variables) { '#message' => $log_message['message'], '#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( '#theme' => 'item_list', @@ -151,13 +154,6 @@ function theme_authorize_report($variables) { * @ingroup themeable */ function theme_authorize_message($variables) { - $message = $variables['message']; - $success = $variables['success']; - 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; + $item = array('#markup' => $variables['message']); + return drupal_render($item); } diff --git a/core/lib/Drupal/Core/FileTransfer/Form/FileTransferAuthorizeForm.php b/core/lib/Drupal/Core/FileTransfer/Form/FileTransferAuthorizeForm.php index 226e93e20ca33b759154ba36a4e6e6ba586897ee..7bc0f1cc518f8e1d0d98389a02bc3c297329c64c 100644 --- a/core/lib/Drupal/Core/FileTransfer/Form/FileTransferAuthorizeForm.php +++ b/core/lib/Drupal/Core/FileTransfer/Form/FileTransferAuthorizeForm.php @@ -11,6 +11,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Response; /** * Provides the file transfer authorization form. @@ -226,7 +227,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $filetransfer = $this->getFiletransfer($filetransfer_backend, $form_connection_settings[$filetransfer_backend]); // Now run the operation. - $this->runOperation($filetransfer); + $response = $this->runOperation($filetransfer); + if ($response instanceof Response) { + $form_state->setResponse($response); + } } catch (\Exception $e) { // If there is no database available, we don't care and just skip @@ -333,13 +337,18 @@ protected function setConnectionSettingsDefaults(&$element, $key, array $default * * @param $filetransfer * 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) { $operation = $_SESSION['authorize_operation']; unset($_SESSION['authorize_operation']); 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'])); } } diff --git a/core/lib/Drupal/Core/Updater/Module.php b/core/lib/Drupal/Core/Updater/Module.php index 25c03f96124db305366996b024659bf894f24be0..c495272796ab07b05e395137113957875f257af5 100644 --- a/core/lib/Drupal/Core/Updater/Module.php +++ b/core/lib/Drupal/Core/Updater/Module.php @@ -20,7 +20,7 @@ class Module extends Updater implements UpdaterInterface { * * 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 - * 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 * where all the documentation recommends users install their modules, and * there's no way that can conflict on a multi-site installation, since @@ -31,20 +31,30 @@ class Module extends Updater implements UpdaterInterface { * A directory path. */ 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); } 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(). */ 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]); } /** diff --git a/core/lib/Drupal/Core/Updater/Theme.php b/core/lib/Drupal/Core/Updater/Theme.php index 8ad55f0d520d9255f02e2366c828f8722827fbf2..519f110b52834fa0feabedc7229d3f5a13d0d7df 100644 --- a/core/lib/Drupal/Core/Updater/Theme.php +++ b/core/lib/Drupal/Core/Updater/Theme.php @@ -20,7 +20,7 @@ class Theme extends Updater implements UpdaterInterface { * * 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 - * 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 * where all the documentation recommends users install their themes, and * there's no way that can conflict on a multi-site installation, since @@ -31,20 +31,30 @@ class Theme extends Updater implements UpdaterInterface { * A directory path. */ 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); } 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(). */ 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]); } /** diff --git a/core/lib/Drupal/Core/Updater/Updater.php b/core/lib/Drupal/Core/Updater/Updater.php index 4f56cd9e8377f62a5afa8ac8e520e34734f48819..4869b1cfec1b75cf2fe89f8e6cb8688b850f5efd 100644 --- a/core/lib/Drupal/Core/Updater/Updater.php +++ b/core/lib/Drupal/Core/Updater/Updater.php @@ -23,14 +23,26 @@ class Updater { */ public $source; + /** + * The root directory under which new projects will be copied. + * + * @var string + */ + protected $root; + /** * Constructs a new updater. * * @param string $source * 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->root = $root; $this->name = self::getProjectName($source); $this->title = self::getProjectTitle($source); } @@ -43,20 +55,24 @@ public function __construct($source) { * * @param string $source * 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 * A new Drupal\Core\Updater\Updater object. * * @throws \Drupal\Core\Updater\UpdaterException */ - public static function factory($source) { + public static function factory($source, $root) { if (is_dir($source)) { $updater = self::getUpdaterFromDirectory($source); } else { throw new UpdaterException(t('Unable to determine the type of the source directory.')); } - return new $updater($source); + return new $updater($source, $root); } /** diff --git a/core/lib/Drupal/Core/Updater/UpdaterInterface.php b/core/lib/Drupal/Core/Updater/UpdaterInterface.php index 7cd7512c6d12220486b7db2fc18915ed8b4f0f50..331e84068ad36b3ed7e8785d6d83d8f8ab08dda3 100644 --- a/core/lib/Drupal/Core/Updater/UpdaterInterface.php +++ b/core/lib/Drupal/Core/Updater/UpdaterInterface.php @@ -35,13 +35,21 @@ public function isInstalled(); 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 * An absolute path to the default install location. */ 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. * diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 027ba8bd20476be96f0a59bf7efd11c5352389b1..d6d98c8f812bb449c69b7942ea775d9109d24f30 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -460,7 +460,7 @@ function system_authorized_run($callback, $file, $arguments = array(), $page_tit function system_authorized_batch_process() { $finish_url = system_authorized_get_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); } /** diff --git a/core/modules/update/src/Form/UpdateManagerInstall.php b/core/modules/update/src/Form/UpdateManagerInstall.php index b37f2464d06e72e2bb201b2a533f82d8a9aa0dc1..bb67ec6c455015c0b3d669072bd9d72838acd749 100644 --- a/core/modules/update/src/Form/UpdateManagerInstall.php +++ b/core/modules/update/src/Form/UpdateManagerInstall.php @@ -13,6 +13,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Updater\Updater; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Response; /** * Configure update settings for this site. @@ -27,7 +28,7 @@ class UpdateManagerInstall extends FormBase { protected $moduleHandler; /** - * The app root. + * The root location under which installed projects will be saved. * * @var string */ @@ -44,7 +45,7 @@ class UpdateManagerInstall extends FormBase { * Constructs a new UpdateManagerInstall. * * @param string $root - * The app root. + * The root location under which installed projects will be saved. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. * @param string $site_path @@ -68,7 +69,7 @@ public function getFormId() { */ public static function create(ContainerInterface $container) { return new static( - $container->get('app.root'), + $container->get('update.root'), $container->get('module_handler'), $container->get('site.path') ); @@ -192,7 +193,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $project_location = $directory . '/' . $project; try { - $updater = Updater::factory($project_location); + $updater = Updater::factory($project_location, $this->root); } catch (\Exception $e) { drupal_set_message($e->getMessage(), 'error'); @@ -231,7 +232,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { if (fileowner($project_real_location) == fileowner($this->sitePath)) { $this->moduleHandler->loadInclude('update', 'inc', 'update.authorize'); $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 diff --git a/core/modules/update/src/Form/UpdateReady.php b/core/modules/update/src/Form/UpdateReady.php index 1162c715dfbfa1157f0c2394318af94095a1365c..7ad4d648d510127bd839ea05112e23d202c52828 100644 --- a/core/modules/update/src/Form/UpdateReady.php +++ b/core/modules/update/src/Form/UpdateReady.php @@ -14,6 +14,7 @@ use Drupal\Core\State\StateInterface; use Drupal\Core\Updater\Updater; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Response; /** * Configure update settings for this site. @@ -21,7 +22,7 @@ class UpdateReady extends FormBase { /** - * The app root. + * The root location under which updated projects will be saved. * * @var string */ @@ -52,7 +53,7 @@ class UpdateReady extends FormBase { * Constructs a new UpdateReady object. * * @param string $root - * The app root. + * The root location under which updated projects will be saved. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The object that manages enabled modules in a Drupal installation. * @param \Drupal\Core\State\StateInterface $state @@ -79,7 +80,7 @@ public function getFormId() { */ public static function create(ContainerInterface $container) { return new static( - $container->get('app.root'), + $container->get('update.root'), $container->get('module_handler'), $container->get('state'), $container->get('site.path') @@ -139,7 +140,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $project_real_location = NULL; foreach ($projects as $project => $url) { $project_location = $directory . '/' . $project; - $updater = Updater::factory($project_location); + $updater = Updater::factory($project_location, $this->root); $project_real_location = drupal_realpath($project_location); $updates[] = array( 'project' => $project, @@ -156,7 +157,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { if (fileowner($project_real_location) == fileowner($this->sitePath)) { $this->moduleHandler->loadInclude('update', 'inc', 'update.authorize'); $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 // credentials and invoke update_authorize_run_update() indirectly with diff --git a/core/modules/update/src/Tests/UpdateTestBase.php b/core/modules/update/src/Tests/UpdateTestBase.php index 21a0d21f4b3d2bad6b6f25f14bf621ffe7c710f8..33ee105d87fe7ab4dce78dfd3c66b54b391d6fd0 100644 --- a/core/modules/update/src/Tests/UpdateTestBase.php +++ b/core/modules/update/src/Tests/UpdateTestBase.php @@ -21,6 +21,7 @@ namespace Drupal\update\Tests; +use Drupal\Core\DrupalKernel; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; @@ -29,6 +30,29 @@ */ 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. * diff --git a/core/modules/update/src/Tests/UpdateUploadTest.php b/core/modules/update/src/Tests/UpdateUploadTest.php index 7636f7ab3ab359670235ac723b683852cc39fb46..c568fa5a1b531a9663a2e468efffa9b8bcd6d0f5 100644 --- a/core/modules/update/src/Tests/UpdateUploadTest.php +++ b/core/modules/update/src/Tests/UpdateUploadTest.php @@ -46,6 +46,7 @@ public function testUploadModule() { // This also checks that the correct archive extensions are allowed. $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->assertUrl('admin/modules/install'); // 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 @@ -56,6 +57,24 @@ public function testUploadModule() { ); $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->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.'); } /** diff --git a/core/modules/update/src/UpdateRootFactory.php b/core/modules/update/src/UpdateRootFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..60a2201065110cbfa494265d5c0f8aeb8bf977b1 --- /dev/null +++ b/core/modules/update/src/UpdateRootFactory.php @@ -0,0 +1,77 @@ +drupalKernel = $drupal_kernel; + $this->requestStack = $request_stack; + } + + /** + * Gets the root path under which projects are installed or updated. + * + * The Update Manager will ensure that project files can only be copied to + * specific subdirectories of this root path. + * + * @return string + */ + public function get() { + // Normally the Update Manager's root path is the same as the app root (the + // directory in which the Drupal site is installed). + $root_path = $this->drupalKernel->getAppRoot(); + + // When running in a test site, change the root path to be the testing site + // directory. This ensures that it will always be writable by the webserver + // (thereby allowing the actual extraction and installation of projects by + // the Update Manager to be tested) and also ensures that new project files + // added there won't be visible to the parent site and will be properly + // cleaned up once the test finishes running. This is done here (rather + // than having the tests enable a module which overrides the update root + // factory service) to ensure that the parent site is automatically kept + // clean without relying on test authors to take any explicit steps. See + // also \Drupal\update\Tests\UpdateTestBase::setUp(). + if (DRUPAL_TEST_IN_CHILD_SITE) { + $kernel = $this->drupalKernel; + $request = $this->requestStack->getCurrentRequest(); + $root_path .= '/' . $kernel::findSitePath($request); + } + + return $root_path; + } + +} diff --git a/core/modules/update/tests/update_test_new_module.tar.gz b/core/modules/update/tests/update_test_new_module.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fba2143bf989971d9e1848a7c67b599d0ab15f4e --- /dev/null +++ b/core/modules/update/tests/update_test_new_module.tar.gz @@ -0,0 +1 @@ +wTAO0+z#0ڲƘA^4JV`m֪{;"&!A}Bulogյm[ApHS*nFb9v."ŤJy$8AIEiVsO۞0+e] XhgB2~疿:$ʛ6yS7m4~FR]Oج,)#gLY"mߚn,+fadFyUat{G&ewrrb;{Ul_(}d fn4=]/7.3]Qf]Ch,}\( \ No newline at end of file diff --git a/core/modules/update/update.authorize.inc b/core/modules/update/update.authorize.inc index d6a70667414589871221290d1c0c55e3c58e03e6..cead4f027eb99054fe4fafcc40737d15ac9881f4 100644 --- a/core/modules/update/update.authorize.inc +++ b/core/modules/update/update.authorize.inc @@ -28,6 +28,11 @@ * - updater_name: The name of the Drupal\Core\Updater\Updater class to use * for this project. * - local_url: The locally installed location of new code to update with. + * + * @return \Symfony\Component\HttpFoundation\Response|null + * The result of processing the batch that updates the projects. If this is + * an instance of \Symfony\Component\HttpFoundation\Response the calling code + * should use that response for the current page request. */ function update_authorize_run_update($filetransfer, $projects) { $operations = array(); @@ -50,8 +55,8 @@ function update_authorize_run_update($filetransfer, $projects) { 'finished' => 'update_authorize_update_batch_finished', 'file' => drupal_get_path('module', 'update') . '/update.authorize.inc', ); - batch_set($batch); + // Invoke the batch via authorize.php. return system_authorized_batch_process(); } @@ -74,6 +79,11 @@ function update_authorize_run_update($filetransfer, $projects) { * @param string $local_url * The URL to the locally installed temp directory where the project has * already been downloaded and extracted into. + * + * @return \Symfony\Component\HttpFoundation\Response|null + * The result of processing the batch that installs the project. If this is + * an instance of \Symfony\Component\HttpFoundation\Response the calling code + * should use that response for the current page request. */ function update_authorize_run_install($filetransfer, $project, $updater_name, $local_url) { $operations[] = array( @@ -147,7 +157,7 @@ function update_authorize_batch_copy_project($project, $updater_name, $local_url return; } - $updater = new $updater_name($local_url); + $updater = new $updater_name($local_url, \Drupal::getContainer()->get('update.root')); try { if ($updater->isInstalled()) { diff --git a/core/modules/update/update.services.yml b/core/modules/update/update.services.yml index c14b8ccdb7ea10009a8aec82928cc3f95a2774c5..fc176d6f7bed9dbffea5fca8207fc4b9ba6d5173 100644 --- a/core/modules/update/update.services.yml +++ b/core/modules/update/update.services.yml @@ -13,3 +13,12 @@ services: update.fetcher: class: Drupal\update\UpdateFetcher arguments: ['@config.factory', '@http_client'] + update.root: + class: SplString + factory: update.root.factory:get + tags: + - { name: parameter_service } + update.root.factory: + class: Drupal\update\UpdateRootFactory + arguments: ['@kernel', '@request_stack'] + public: false