From 2663cbbf0779625a0c90fce3d77a6f8cf30af8ca Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Thu, 27 Jun 2024 17:43:40 +0000 Subject: [PATCH] Issue #3457376 by phenaproxima, tim.plunkett, chrisfromredfin: Use a route enhancer to move away from having project ID hashes in URLs --- .../ProjectBrowserSource/RandomDataPlugin.php | 1 + project_browser.routing.yml | 3 +- project_browser.services.yml | 4 + src/Controller/BrowserController.php | 10 +- src/Controller/InstallerController.php | 52 +++++------ src/EnabledSourceHandler.php | 12 +-- .../ProjectBrowserSource/DrupalCore.php | 3 +- .../ProjectBrowserSource/MockDrupalDotOrg.php | 1 + src/ProjectBrowser/Project.php | 19 ++++ src/Routing/ProjectBrowserRoutes.php | 40 +++++++- src/Routing/ProjectEnhancer.php | 61 ++++++++++++ sveltejs/public/build/bundle.js | Bin 452824 -> 452696 bytes sveltejs/public/build/bundle.js.map | Bin 310149 -> 310101 bytes sveltejs/src/App.svelte | 5 +- .../Functional/InstallerControllerTest.php | 87 +++++++----------- .../ProjectBrowserUiTest.php | 3 +- 16 files changed, 199 insertions(+), 102 deletions(-) create mode 100644 src/Routing/ProjectEnhancer.php diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php index 7abcf406f..82fb36148 100644 --- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php +++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php @@ -200,6 +200,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { packageName: 'random/' . $machine_name, categories: [$categories[array_rand($categories)]], images: $project_images, + id: $machine_name, ); } $this->cacheBin->set('RandomData:projects', $projects); diff --git a/project_browser.routing.yml b/project_browser.routing.yml index 5c15191d0..b7ad1762b 100644 --- a/project_browser.routing.yml +++ b/project_browser.routing.yml @@ -19,11 +19,12 @@ project_browser.api_project_get_all: #options: # no_cache: 'TRUE' project_browser.browse: - path: '/admin/modules/browse/{id}' + path: '/admin/modules/browse/{source}/{id}' defaults: _controller: '\Drupal\project_browser\Controller\BrowserController::browse' _title: 'Browse projects' id: null + source: null requirements: _permission: 'administer modules' project_browser.settings: diff --git a/project_browser.services.yml b/project_browser.services.yml index fec72d8eb..7152710c4 100644 --- a/project_browser.services.yml +++ b/project_browser.services.yml @@ -35,3 +35,7 @@ services: public: false tags: - { name: project_browser.activator } + Drupal\project_browser\Routing\ProjectEnhancer: + public: false + tags: + - { name: route_enhancer } diff --git a/src/Controller/BrowserController.php b/src/Controller/BrowserController.php index da1d0fbcd..d19796c2d 100644 --- a/src/Controller/BrowserController.php +++ b/src/Controller/BrowserController.php @@ -63,18 +63,22 @@ class BrowserController extends ControllerBase { * rendered. For example, 'https//drupal-site/admin/modules/browse/ctools' * will display the details for ctools. * + * @param string|null $source + * (optional) If viewing a specific project, the ID of the source plugin + * that exposed it. * @param string|null $id - * The project ID, if any. + * (optional) If viewing a specific project, the project's local ID (as + * known to the source plugin). * * @return array * A render array. */ - public function browse(?string $id = NULL) { + public function browse(?string $source, ?string $id) { $request = $this->requestStack->getCurrentRequest(); $current_sources = $this->enabledSource->getCurrentSources(); $ui_install_enabled = (bool) $this->config('project_browser.admin_settings')->get('allow_ui_install') && (bool) $this->installReadiness; - if (array_key_exists('drupalorg_mockapi', $current_sources) && empty($id)) { + if (array_key_exists('drupalorg_mockapi', $current_sources) && (empty($source) || empty($id))) { $this->messenger() ->addStatus($this->t('Project Browser is currently a prototype, and the projects listed may not be up to date with Drupal.org. For the most updated list of projects, visit <a href=":url">:url</a>', [':url' => 'https://www.drupal.org/project/project_module'])) ->addStatus($this->t('Your feedback and input are welcome at <a href=":url">:url</a>', [':url' => 'https://www.drupal.org/project/issues/project_browser'])); diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 7020569ed..7209ad0e5 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -13,6 +13,7 @@ use Drupal\package_manager\Exception\StageException; use Drupal\project_browser\ActivatorInterface; use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; +use Drupal\project_browser\ProjectBrowser\Project; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -141,8 +142,8 @@ class InstallerController extends ControllerBase { /** * Returns the status of the project in the temp store. * - * @param string $uuid - * The UUID of the project, as known to the enabled sources handler. + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * A project whose status to report. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Information about the project's require/install status. @@ -154,16 +155,16 @@ class InstallerController extends ControllerBase { * regularly so it can monitor the progress of the process and report which * stage is taking place. */ - public function inProgress(string $uuid): JsonResponse { + public function inProgress(Project $project): JsonResponse { $requiring = $this->projectBrowserTempStore->get('requiring'); $core_installing = $this->projectBrowserTempStore->get('installing'); $return = ['status' => self::STATUS_IDLE]; - if (isset($requiring['project_id']) && $requiring['project_id'] === $uuid) { + if (isset($requiring['project_id']) && $requiring['project_id'] === $project->id) { $return['status'] = self::STATUS_REQUIRING_PROJECT; $return['phase'] = $requiring['phase']; } - if ($core_installing === $uuid) { + if ($core_installing === $project->id) { $return['status'] = self::STATUS_INSTALLING_PROJECT; } @@ -243,18 +244,18 @@ class InstallerController extends ControllerBase { /** * Updates the 'requiring' state in the temp store. * - * @param string $uuid - * The UUID of the project being required, as known to the enabled sources + * @param string $id + * The ID of the project being required, as known to the enabled sources * handler. * @param string $phase * The require phase in progress. * @param string|null $stage_id * The stage ID, if known. */ - private function setRequiringState(?string $uuid, string $phase, ?string $stage_id): void { + private function setRequiringState(?string $id, string $phase, ?string $stage_id): void { $data = $this->projectBrowserTempStore->get('requiring') ?? []; - if ($uuid) { - $data['project_id'] = $uuid; + if ($id) { + $data['project_id'] = $id; } if ($stage_id) { $data['stage_id'] = $stage_id; @@ -329,18 +330,17 @@ class InstallerController extends ControllerBase { /** * Begins requiring by creating a stage. * - * @param string $uuid - * The UUID of the project, as known to the enabled sources handler. + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * The project we're about to require. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function begin(string $uuid): JsonResponse { - $project = $this->enabledSourceHandler->getStoredProject($uuid); + public function begin(Project $project): JsonResponse { // @todo Expand to support other plugins in https://drupal.org/i/3312354. $source = $this->enabledSourceHandler->getCurrentSources()['drupalorg_mockapi'] ?? NULL; if ($source === NULL) { - return new JsonResponse(['message' => "Cannot download $uuid from any available source"], 500); + return new JsonResponse(['message' => "Cannot download $project->id from any available source"], 500); } if (!$source->isProjectSafe($project)) { return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500); @@ -408,24 +408,24 @@ class InstallerController extends ControllerBase { /** * Performs require operations on the stage. * - * @param string $uuid - * The UUID of the project, as known to the enabled sources handler. + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * The project to be required. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function require(string $uuid): JsonResponse { + public function require(Project $project): JsonResponse { $requiring = $this->projectBrowserTempStore->get('requiring'); - if (empty($requiring['project_id']) || $requiring['project_id'] !== $uuid) { + if (empty($requiring['project_id']) || $requiring['project_id'] !== $project->id) { return new JsonResponse([ - 'message' => sprintf('Error: a request to install %s was ignored as an install for a different module is in progress.', $uuid), + 'message' => sprintf('Error: a request to install %s was ignored as an install for a different module is in progress.', $project->id), ], 500); } $this->setRequiringState(NULL, 'requiring module', NULL); try { $this->installer->claim($requiring['stage_id'])->require([ - $this->enabledSourceHandler->getStoredProject($uuid)->packageName, + $project->packageName, ]); return $this->successResponse('require', $requiring['stage_id']); } @@ -498,17 +498,17 @@ class InstallerController extends ControllerBase { /** * Installs an already downloaded module. * - * @param string $uuid - * The UUID of the project, as known to the enabled sources handler. + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * The project to activate. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function activate(string $uuid): JsonResponse { - $this->projectBrowserTempStore->set('installing', $uuid); + public function activate(Project $project): JsonResponse { + $this->projectBrowserTempStore->set('installing', $project->id); try { - $this->activator->activate($this->enabledSourceHandler->getStoredProject($uuid)); + $this->activator->activate($project); } catch (\Throwable $e) { return $this->errorResponse($e, 'project install'); diff --git a/src/EnabledSourceHandler.php b/src/EnabledSourceHandler.php index 2a56fac20..4a242d199 100644 --- a/src/EnabledSourceHandler.php +++ b/src/EnabledSourceHandler.php @@ -115,13 +115,9 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter $stored = []; foreach ($projects as $source_id => $results) { foreach ($results->list as $project) { - // Generate an ID for the project from the package name and machine - // name, which are unlikely to change. This isn't security-sensitive, - // so SHA1 is okay for this purpose. - $project->id = sha1($source_id . $project->packageName . $project->machineName); - // Remember the ID of the source plugin that exposed this project, - // since that information might be needed by the front-end. - $project->source = $source_id; + // Prefix the local project ID with the source plugin ID, so we can + // look it up unambiguously. + $project->id = $source_id . '/' . $project->id; $this->keyValue->setIfNotExists($project->id, $project); // Add activation data to the project. This is volatile and should not @@ -223,6 +219,8 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter $project->status = $this->activator->getStatus($project); // The activator is responsible for generating the instructions. $project->commands = $this->activator->getInstructions($project); + // Give the front-end the ID of the source plugin that exposed this project. + [$project->source] = explode('/', $project->id, 2); } } diff --git a/src/Plugin/ProjectBrowserSource/DrupalCore.php b/src/Plugin/ProjectBrowserSource/DrupalCore.php index a839ac053..aea3c5369 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalCore.php +++ b/src/Plugin/ProjectBrowserSource/DrupalCore.php @@ -205,13 +205,14 @@ class DrupalCore extends ProjectBrowserSourceBase { author: [ 'name' => 'Drupal Core', ], - packageName: '', + packageName: 'drupal/core', categories: [ [ 'id' => $module->info['package'], 'name' => $module->info['package'], ], ], + id: $module_name, ); } diff --git a/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php b/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php index 3fe3d3c58..12df3f57e 100644 --- a/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php +++ b/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php @@ -397,6 +397,7 @@ class MockDrupalDotOrg extends ProjectBrowserSourceBase { categories: array_map(fn($category) => $categories[$category['id']] ?? '', $project_data['project_data']['taxonomy_vocabulary_3'] ?? []), images: $project_data['project_data']['field_project_images'] ?? [], warnings: $this->getWarnings($project_data), + id: $project_data['field_project_machine_name'], ); } } diff --git a/src/ProjectBrowser/Project.php b/src/ProjectBrowser/Project.php index f861e4ed3..57d83c74d 100644 --- a/src/ProjectBrowser/Project.php +++ b/src/ProjectBrowser/Project.php @@ -105,6 +105,12 @@ class Project implements \JsonSerializable { * @param string|ProjectType $type * The project type. Defaults to a module, but may be any string that is not * one of the cases of \Drupal\project_browser\ProjectType. + * @param string|null $id + * (optional) A local, source plugin-specific identifier for this project. + * Cannot contain slashes. Will be automatically generated if not passed. + * + * @throws \InvalidArgumentException + * Thrown if $id contains slashes. */ public function __construct( public array $logo, @@ -126,6 +132,7 @@ class Project implements \JsonSerializable { public array $images = [], public array $warnings = [], string|ProjectType $type = ProjectType::Module, + ?string $id = NULL, ) { $this->setSummary($body); @@ -134,6 +141,18 @@ class Project implements \JsonSerializable { $type = ProjectType::tryFrom($type) ?? $type; } $this->type = $type; + + // If no local ID was passed, generate it from the package name and machine + // name, which are unlikely to change. + if (empty($id)) { + $id = str_replace('/', '-', [$packageName, $machineName]); + $id = implode('-', $id); + $id = trim($id, '-'); + } + if (str_contains($id, '/')) { + throw new \InvalidArgumentException("The project ID cannot contain slashes."); + } + $this->id = $id; } /** diff --git a/src/Routing/ProjectBrowserRoutes.php b/src/Routing/ProjectBrowserRoutes.php index fd8939c2b..e5dc80ba8 100644 --- a/src/Routing/ProjectBrowserRoutes.php +++ b/src/Routing/ProjectBrowserRoutes.php @@ -47,26 +47,42 @@ class ProjectBrowserRoutes implements ContainerInjectionInterface { } $routes = []; $routes['project_browser.stage.begin'] = new Route( - '/admin/modules/project_browser/install-begin/{uuid}', + '/admin/modules/project_browser/install-begin/{source}/{id}', [ '_controller' => InstallerController::class . '::begin', '_title' => 'Create phase', + 'project' => NULL, ], [ '_permission' => 'administer modules', '_custom_access' => InstallerController::class . '::access', ], + [ + 'parameters' => [ + 'project' => [ + 'project_browser.project' => ['source', 'id'], + ], + ], + ], ); $routes['project_browser.stage.require'] = new Route( - '/admin/modules/project_browser/install-require/{uuid}', + '/admin/modules/project_browser/install-require/{source}/{id}', [ '_controller' => InstallerController::class . '::require', '_title' => 'Require phase', + 'project' => NULL, ], [ '_permission' => 'administer modules', '_custom_access' => InstallerController::class . '::access', ], + [ + 'parameters' => [ + 'project' => [ + 'project_browser.project' => ['source', 'id'], + ], + ], + ] ); $routes['project_browser.stage.apply'] = new Route( '/admin/modules/project_browser/install-apply', @@ -102,26 +118,42 @@ class ProjectBrowserRoutes implements ContainerInjectionInterface { ], ); $routes['project_browser.activate'] = new Route( - '/admin/modules/project_browser/activate/{uuid}', + '/admin/modules/project_browser/activate/{source}/{id}', [ '_controller' => InstallerController::class . '::activate', '_title' => 'Install module in core', + 'project' => NULL, ], [ '_permission' => 'administer modules', '_custom_access' => InstallerController::class . '::access', ], + [ + 'parameters' => [ + 'project' => [ + 'project_browser.project' => ['source', 'id'], + ], + ], + ] ); $routes['project_browser.module.install_in_progress'] = new Route( - '/admin/modules/project_browser/install_in_progress/{uuid}', + '/admin/modules/project_browser/install_in_progress/{source}/{id}', [ '_controller' => InstallerController::class . '::inProgress', '_title' => 'Install in progress', + 'project' => NULL, ], [ '_permission' => 'administer modules', '_custom_access' => InstallerController::class . '::access', ], + [ + 'parameters' => [ + 'project' => [ + 'project_browser.project' => ['source', 'id'], + ], + ], + ] ); $routes['project_browser.install.unlock'] = new Route( '/admin/modules/project_browser/install/unlock', diff --git a/src/Routing/ProjectEnhancer.php b/src/Routing/ProjectEnhancer.php new file mode 100644 index 000000000..4cc95f0a2 --- /dev/null +++ b/src/Routing/ProjectEnhancer.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\Routing; + +use Drupal\Core\Routing\EnhancerInterface; +use Drupal\Core\Routing\RouteObjectInterface; +use Drupal\project_browser\EnabledSourceHandler; +use Symfony\Component\HttpFoundation\Request; + +/** + * Converts route parameters into a Project object. + * + * If a route parameter defines a `project_browser.project` option, this will + * add a Project object to the defaults, getting the source plugin ID and local + * (source-specific) project ID from other route parameters. + * + * For example, consider a route like this: + * ``` + * path: '/projects/view/{source}/{id}' + * defaults: + * project: null + * options: + * parameters: + * project: + * project_browser.project: [source, id] + * ``` + * This will look up a project using the `source` parameter as the source plugin + * ID, and the `id` parameter as the project's local ID. The project will be + * passed to the controller's $project parameter. + */ +final class ProjectEnhancer implements EnhancerInterface { + + public function __construct( + private readonly EnabledSourceHandler $enabledSources, + ) {} + + /** + * {@inheritdoc} + */ + public function enhance(array $defaults, Request $request): array { + /** @var \Symfony\Component\Routing\Route $route */ + $route = $defaults[RouteObjectInterface::ROUTE_OBJECT]; + + $parameters = $route->getOption('parameters'); + foreach (array_keys($defaults) as $name) { + if (isset($parameters[$name]['project_browser.project'])) { + [$source_id, $local_id] = $parameters[$name]['project_browser.project']; + + if (array_key_exists($source_id, $defaults) && array_key_exists($local_id, $defaults)) { + // @see \Drupal\project_browser\EnabledSourceHandler::getProjects() + $id = $defaults[$source_id] . '/' . $defaults[$local_id]; + $defaults[$name] = $this->enabledSources->getStoredProject($id); + } + } + } + return $defaults; + } + +} diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index cb9f528c49d6673100aae26249b18ef014e4a50a..2b56daeab0ad74334cc8e4da0b00a122694421e3 100644 GIT binary patch delta 253 zcmV<Z00RHm(Hq#%8-RoXgaU*Egam{Iv<7Fd12;M_m+<HVD3^9y1`(I)djuV~z^?}A z1p_xaFqa@01VWcv1p^DW__YR=1D6ja0}7Xyxd!hFVPkY@c4bsyVj!2HEddsn9Eb!9 zx4*gussWdGyaq)9GPk|F2DAtPGM94222GctnF1@f>cs|i0|7OcV95qW0W-Iz$p*p$ z0W+6r%?42cIJdja1`Y-RIG4)P22cVsIkz0t24w>RGdY(#sRCZN;nxPX2LUdZo8Jap zw+7$_t^=3I;|5ZfkFo;?mo4N5)0a<Y1P7Nd-vb+$$>0VJm%ZQv9hXmM1P8Z;<pxs& DUJ_nW delta 327 zcmca{LHfo;>4p}@7N!>F7M2#)Eo|xQnJuggrhj<BtTDYTnoV%}t7;b6?FZJgy<i1P zC`hn4F&Ud}f4`M&9y6oa^uC>JKO~A%6N{2Ff=h}r^U`$`5|c|Z%ThxUlN6>e)MXT# zF4Muny?x^@wpEP0C<>=X?O}6cG~T{&58D<FM&s!~T6lWqQ8p`PGb<CY3$~v-%2vt@ z=7Q}tF`4drmQ8&6C38l(?fXx%^)fSpMFJTux6ePz#>d8JIUPugPv2w9V##D;u<Z(4 zD)aQUj~F?oJFaAk*nadn+ctKV=s5k@={5J+Vy6F`33S<&`)r$-r!Q?}5}CeU66oRS zkJ%hqQW8rNr(bx?_Gr439GlVfe^1!hr|aKmmYzQQ0UPi1y$_gWr~9X~uy1F5$`;HF E0Q#?aPyhe` diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index aa1574549a7b322482df69a1a5d9b7d3642cb6b2..278fb3a1cdfc0afd8df9d95b8f99126679db8852 100644 GIT binary patch delta 123 zcmZquE_C&qP(usj7N(!`r`LHi$xZj&#K<*We*sesqu%uO3z%ZtB^NRQG4poGg)E1K zS+blmT&6RAU>2P=gPC`_UoA7wcD}7F8<`l5rytnPVkVmI?C6;8tOFz+!GyJ^qpRt3 Y?j0=Y+Z%VVlrk|pI-5@avx}t@0N?K_wg3PC delta 182 zcmccmO{n#|P(usj7N(!`d5cpMi;^>fONuh{(sicSdNavS_ua(EHGSp;Cgte|mIGOS z3z+;_qT}>qr_Wo!6w8v5Sd!TOX#o=uGjIR2faQ!ZW5)LVTUZ`3@tQgX==eK2`s=tm zI=W9cRAmvJzP*n{P}14dStr-o(J|Lq2S_@C32RSBSN-X4x3i@4c{)0JLd1d{oxP^_ a>$8Y%&)C6|$i!xCZSCl6KK;xtmQDb4hCj0a diff --git a/sveltejs/src/App.svelte b/sveltejs/src/App.svelte index 6bcf837da..04f69fd20 100644 --- a/sveltejs/src/App.svelte +++ b/sveltejs/src/App.svelte @@ -2,16 +2,15 @@ import ProjectBrowser from './ProjectBrowser.svelte'; import ModulePage from './ModulePage.svelte'; import Loading from './Loading.svelte'; - import { searchString, activeTab } from './stores'; + import { activeTab } from './stores'; import { ORIGIN_URL } from './constants'; const matches = window.location.pathname.match( - /\/admin\/modules\/browse\/([^/]+)/, + /\/admin\/modules\/browse\/(.+)/, ); const projectId = matches ? matches[1] : null; let loading = true; - let data; let project = []; let projectExists = false; async function load(url) { diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index e3276054a..fcb0d42a8 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -180,6 +180,9 @@ class InstallerControllerTest extends BrowserTestBase { ->set('enabled_sources', ['drupalorg_mockapi', 'drupal_core']) ->set('allow_ui_install', TRUE) ->save(); + + // Prime the non-volatile cache. + $this->container->get(EnabledSourceHandler::class)->getProjects(); } /** @@ -189,7 +192,7 @@ class InstallerControllerTest extends BrowserTestBase { */ public function testUiInstallUnavailableIfDisabled() { $this->config('project_browser.admin_settings')->set('allow_ui_install', FALSE)->save(); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('awesome_module')); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(403); $this->assertSession()->pageTextContains('Access denied'); } @@ -201,7 +204,7 @@ class InstallerControllerTest extends BrowserTestBase { */ public function testInstallSecurityRevokedModule() { $this->assertProjectBrowserTempStatus(NULL, NULL); - $content = $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('security_revoked_module')); + $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/security_revoked_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"security_revoked_module is not safe to add because its security coverage has been revoked"}', $content); } @@ -216,10 +219,9 @@ class InstallerControllerTest extends BrowserTestBase { // Though core is not available as a choice in project browser, it works // well for the purposes of this test as it's definitely already added // via composer. - $project_id = $this->getProjectUuid('core'); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/core'); $this->stageId = $this->sharedTempStore->get('package_manager_stage')->get('lock')[0]; - $content = $this->drupalGet("/admin/modules/project_browser/install-require/$project_id"); + $content = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/core"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: The following package is already installed: drupal\/core\n","phase":"require"}', $content); } @@ -231,13 +233,12 @@ class InstallerControllerTest extends BrowserTestBase { */ private function doStart() { $this->assertProjectBrowserTempStatus(NULL, NULL); - $project_id = $this->getProjectUuid('awesome_module'); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->stageId = $this->sharedTempStore->get('package_manager_stage')->get('lock')[0]; $this->assertSession()->statusCodeEquals(200); $expected_output = sprintf('{"phase":"create","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress($project_id, 'creating install stage'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'creating install stage'); } /** @@ -246,11 +247,10 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::require */ private function doRequire() { - $project_id = $this->getProjectUuid('awesome_module'); - $this->drupalGet("/admin/modules/project_browser/install-require/$project_id"); + $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $expected_output = sprintf('{"phase":"require","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress($project_id, 'requiring module'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'requiring module'); } /** @@ -262,7 +262,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->drupalGet("/admin/modules/project_browser/install-apply"); $expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress($this->getProjectUuid('awesome_module'), 'applying'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'applying'); } /** @@ -274,7 +274,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->drupalGet("/admin/modules/project_browser/install-post_apply"); $expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress($this->getProjectUuid('awesome_module'), 'post apply'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'post apply'); } /** @@ -309,7 +309,7 @@ class InstallerControllerTest extends BrowserTestBase { $message = t('This is a PreCreate error.'); $result = ValidationResult::createError([$message]); TestSubscriber::setTestResult([$result], PreCreateEvent::class); - $contents = $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('awesome_module')); + $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: This is a PreCreate error.\n","phase":"create"}', $contents); } @@ -322,7 +322,7 @@ class InstallerControllerTest extends BrowserTestBase { public function testPreCreateException() { $error = new \Exception('PreCreate did not go well.'); TestSubscriber::setException($error, PreCreateEvent::class); - $contents = $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('awesome_module')); + $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PreCreate did not go well.","phase":"create"}', $contents); } @@ -335,7 +335,7 @@ class InstallerControllerTest extends BrowserTestBase { public function testPostCreateException() { $error = new \Exception('PostCreate did not go well.'); TestSubscriber::setException($error, PostCreateEvent::class); - $contents = $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('awesome_module')); + $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PostCreate did not go well.","phase":"create"}', $contents); } @@ -350,7 +350,7 @@ class InstallerControllerTest extends BrowserTestBase { $result = ValidationResult::createError([$message]); $this->doStart(); TestSubscriber::setTestResult([$result], PreRequireEvent::class); - $contents = $this->drupalGet("/admin/modules/project_browser/install-require/" . $this->getProjectUuid('awesome_module')); + $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: This is a PreRequire error.\n","phase":"require"}', $contents); } @@ -364,7 +364,7 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PreRequire did not go well.'); TestSubscriber::setException($error, PreRequireEvent::class); $this->doStart(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-require/" . $this->getProjectUuid('awesome_module')); + $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PreRequire did not go well.","phase":"require"}', $contents); } @@ -378,7 +378,7 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PostRequire did not go well.'); TestSubscriber::setException($error, PostRequireEvent::class); $this->doStart(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-require/" . $this->getProjectUuid('awesome_module')); + $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PostRequire did not go well.","phase":"require"}', $contents); } @@ -437,40 +437,39 @@ class InstallerControllerTest extends BrowserTestBase { */ public function testInstallUnlockMessage() { $this->doStart(); - $project_id = $this->getProjectUuid('awesome_module'); // Check for mid install unlock offer message. - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked less than 1 minutes ago. This is recent enough that a legitimate installation may be in progress. Consider waiting before unlocking the installation staging area.","unlock_url":".*admin..modules..project_browser..install..unlock\?token=[a-zA-Z0-9_-]*"}/', $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress($project_id, 'creating install stage'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'creating install stage'); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); TestTime::setFakeTimeByOffset("+800 seconds"); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 13 minutes ago.","unlock_url":".*admin..modules..project_browser..install..unlock\?token=[a-zA-Z0-9_-]*"}/', $this->getSession()->getPage()->getContent()); $this->doRequire(); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); $this->doApply(); TestTime::setFakeTimeByOffset('+800 seconds'); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertTrue($this->installer->isApplying()); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 13 minutes ago. It should not be unlocked as the changes from staging are being applied to the site.","unlock_url":""}/', $this->getSession()->getPage()->getContent()); TestTime::setFakeTimeByOffset("+55 minutes"); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 55 minutes ago. It should not be unlocked as the changes from staging are being applied to the site.","unlock_url":""}/', $this->getSession()->getPage()->getContent()); // Unlocking the stage becomes possible after 1 hour regardless of source. TestTime::setFakeTimeByOffset("+75 minutes"); - $this->drupalGet('admin/modules/project_browser/install-begin/' . $project_id); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 1 hours, 15 minutes ago.","unlock_url":".*admin..modules..project_browser..install..unlock\?token=[a-zA-Z0-9_-]*"}/', $this->getSession()->getPage()->getContent()); } @@ -486,7 +485,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->doStart(); // Try beginning another install while one is in progress, but not yet in // the applying stage. - $content = $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('metatag')); + $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/metatag'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); @@ -510,7 +509,7 @@ class InstallerControllerTest extends BrowserTestBase { public function testCanBreakStageWithMissingProjectBrowserLock() { $this->doStart(); $this->sharedTempStore->get('project_browser')->delete('requiring'); - $content = $this->drupalGet('admin/modules/project_browser/install-begin/' . $this->getProjectUuid('metatag')); + $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/metatag'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); @@ -538,7 +537,7 @@ class InstallerControllerTest extends BrowserTestBase { $assert_session->checkboxNotChecked('edit-modules-views-enable'); $assert_session->checkboxNotChecked('edit-modules-views-ui-enable'); - $content = $this->drupalGet('admin/modules/project_browser/activate/' . $this->getProjectUuid('views_ui')); + $content = $this->drupalGet('admin/modules/project_browser/activate/drupal_core/views_ui'); $this->assertSame('{"status":0}', $content); $this->rebuildContainer(); $this->drupalGet('admin/modules'); @@ -554,9 +553,9 @@ class InstallerControllerTest extends BrowserTestBase { */ protected function assertInstallNotInProgress($module) { $this->assertProjectBrowserTempStatus(NULL, NULL); - $this->drupalGet("/admin/modules/project_browser/install_in_progress/" . $this->getProjectUuid($module)); + $this->drupalGet("/admin/modules/project_browser/install_in_progress/drupalorg_mockapi/$module"); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); - $this->drupalGet('/admin/modules/project_browser/install_in_progress/' . $this->getProjectUuid('metatag')); + $this->drupalGet('/admin/modules/project_browser/install_in_progress/drupalorg_mockapi/metatag'); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); } @@ -579,7 +578,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->assertProjectBrowserTempStatus($expect_install, NULL); $this->drupalGet("/admin/modules/project_browser/install_in_progress/$project_id"); $this->assertSame(sprintf('{"status":1,"phase":"%s"}', $phase), $this->getSession()->getPage()->getContent()); - $this->drupalGet('/admin/modules/project_browser/install_in_progress/' . $this->getProjectUuid('metatag')); + $this->drupalGet('/admin/modules/project_browser/install_in_progress/drupalorg_mockapi/metatag'); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); } @@ -610,26 +609,4 @@ class InstallerControllerTest extends BrowserTestBase { $this->assertSame($expected_installing, $project_browser_installing); } - /** - * Looks up a project UUID by module name (and optional package name). - * - * @param string $module_name - * The name of the module whose project to find. - * - * @return string - * The project's UUID. - */ - private function getProjectUuid(string $module_name): string { - $projects = $this->container->get(EnabledSourceHandler::class) - ->getProjects(); - foreach ($projects as $results_page) { - foreach ($results_page->list as $project) { - if ($project->machineName === $module_name) { - return $project->id; - } - } - } - $this->fail("There is no project for module '$module_name'."); - } - } diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index c230f9781..688b4dc1a 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -590,8 +590,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Helvetica'); $page->clickLink('Helvetica'); $this->assertTrue($assert_session->waitForText('By Hel Vetica')); - // cspell:disable-next-line - $assert_session->addressEquals('/admin/modules/browse/' . sha1('drupalorg_mockapidrupal/helveticahelvetica')); + $assert_session->addressEquals('/admin/modules/browse/drupalorg_mockapi/helvetica'); $page->clickLink('Back to Browsing'); $assert_session->addressEquals('admin/modules/browse'); } -- GitLab