Verified Commit 8cb28259 authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #3351750 by benjifisher, Rassoni, smustgrave, larowlan, AaronMcHale:...

Issue #3351750 by benjifisher, Rassoni, smustgrave, larowlan, AaronMcHale: Create BC redirects for children of changed paths

(cherry picked from commit 970abeb4)
parent 1ca7a1b2
Loading
Loading
Loading
Loading
+110 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Routing;

use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;

/**
 * Provides helper functions for handling path changes.
 *
 * When a route's path changes, we temporarily add a route to handle the old
 * path and redirect to the new one. This temporary route is for backwards
 * compatibility (BC). If the original route is example.route, then the BC route
 * should be named example.route.bc.
 *
 * The controller for the BC route should have an deprecated annotation, a
 * deprecation error, and type declarations for any parameters that are required
 * for access checking. Then the body of the controller can use the methods
 * provided by this class:
 *
 * @code
 * $change_record = 'https://www.drupal.org/node/3320855';
 * $helper = new PathChangedHelper($route_match, $request);
 * $params = [
 *   '%old_path' => $helper->oldPath(),
 *   '%new_path' => $helper->newPath(),
 *   '%change_record' => $change_record,
 *  ];
 * $this->logger->warning('A user was redirected from %old_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);
 * $message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params);
 * $this->messenger()->addWarning($message);
 * return $helper->redirect();
 * @endcode
 */
class PathChangedHelper {

  /**
   * The URL object for the route whose path has changed.
   *
   * @var \Drupal\Core\Url
   */
  protected Url $newUrl;

  /**
   * The URL object for the BC route.
   *
   * @var \Drupal\Core\Url
   */
  protected Url $oldUrl;

  /**
   * Constructs a PathChangedHelper object.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   A route match object, used for the route name and the parameters.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   A request object, used for the query parameters.
   *
   * @throws \InvalidArgumentException
   *   The route name from $route_match must end with ".bc".
   */
  public function __construct(RouteMatchInterface $route_match, Request $request) {
    $bc_route_name = $route_match->getRouteName();
    if (!str_ends_with($bc_route_name, '.bc')) {
      throw new \InvalidArgumentException(__CLASS__ . ' expects a route name that ends with ".bc".');
    }
    // Strip '.bc' from the end of the route name.
    $route_name = substr($bc_route_name, 0, -3);
    $args = $route_match->getRawParameters()->all();
    $options = [
      'absolute' => TRUE,
      'query' => array_diff_key($request->query->all(), ['destination' => '']),
    ];

    $this->newUrl = Url::fromRoute($route_name, $args, $options);
    $this->oldUrl = Url::fromRoute($bc_route_name, $args, $options);
  }

  /**
   * Returns the deprecated path.
   *
   * @return string
   *   The internal path of the old URL.
   */
  public function oldPath(): string {
    return $this->oldUrl->getInternalPath();
  }

  /**
   * Returns the updated path.
   *
   * @return string
   *   The internal path of the new URL.
   */
  public function newPath(): string {
    return $this->newUrl->getInternalPath();
  }

  /**
   * Returns a redirect to the new path.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirect response.
   */
  public function redirect(): RedirectResponse {
    return new RedirectResponse($this->newUrl->toString(), 301);
  }

}
+14 −1
Original line number Diff line number Diff line
@@ -21,6 +21,19 @@ entity.block_content_type.collection.bc:
  requirements:
    _permission: 'administer block types'

# @todo Deprecate this route once
#   https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove
#   it in Drupal 11.
# @see https://www.drupal.org/node/3320855
entity.block_content_type.edit_form.bc:
  path: '/admin/structure/block/block-content/manage/{block_content_type}'
  defaults:
    _controller: '\Drupal\block_content\Controller\BlockContentController::blockContentTypeRedirect'
  options:
    _admin_route: TRUE
  requirements:
    _permission: 'administer block types'

block_content.add_form:
  path: '/block/add/{block_content_type}'
  defaults:
@@ -84,7 +97,7 @@ entity.block_content.delete_form:
entity.block_content.delete_form.bc:
  path: '/block/{block_content}/delete'
  defaults:
    _controller: '\Drupal\block_content\Controller\BlockContentController::deleteRedirect'
    _controller: '\Drupal\block_content\Controller\BlockContentController::editRedirect'
  options:
    _admin_route: TRUE
  requirements:
+5 −0
Original line number Diff line number Diff line
@@ -4,3 +4,8 @@ services:
    arguments: ['@cache.bootstrap', '@lock', '@entity_type.manager']
    tags:
      - { name: needs_destruction }
  block_content.bc_subscriber:
    class: Drupal\block_content\Routing\RouteSubscriber
    arguments: ['@entity_type.manager', '@module_handler']
    tags:
      - { name: event_subscriber }
+35 −48
Original line number Diff line number Diff line
@@ -4,6 +4,8 @@

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\PathChangedHelper;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\block_content\BlockContentInterface;
use Drupal\block_content\BlockContentTypeInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
@@ -141,6 +143,11 @@ public function getAddFormTitle(BlockContentTypeInterface $block_content_type) {
  /**
   * Provides a redirect to the list of block types.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   A route match object, used for the route name and the parameters.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *
   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use
@@ -149,24 +156,29 @@ public function getAddFormTitle(BlockContentTypeInterface $block_content_type) {
   *
   * @see https://www.drupal.org/node/3320855
   */
  public function blockContentTypeRedirect(): RedirectResponse {
  public function blockContentTypeRedirect(RouteMatchInterface $route_match, Request $request): RedirectResponse {
    @trigger_error('The path /admin/structure/block/block-content/types is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/structure/block-content. See https://www.drupal.org/node/3320855.', E_USER_DEPRECATED);
    $route = 'entity.block_content_type.collection';
    $helper = new PathChangedHelper($route_match, $request);
    $params = [
      '%old_path' => Url::fromRoute("$route.bc")->toString(),
      '%new_path' => Url::fromRoute($route)->toString(),
      '%old_path' => $helper->oldPath(),
      '%new_path' => $helper->newPath(),
      '%change_record' => 'https://www.drupal.org/node/3320855',
    ];
    $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params);
    $this->messenger()->addWarning($warning_message);
    $this->getLogger('block_content')->warning('A user was redirected from %old_path to %new_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);
    $this->getLogger('block_content')->warning('A user was redirected from %old_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);

    return $this->redirect($route, [], [], 301);
    return $helper->redirect();
  }

  /**
   * Provides a redirect to the content block library.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   A route match object, used for the route name and the parameters.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *
   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use
@@ -175,25 +187,29 @@ public function blockContentTypeRedirect(): RedirectResponse {
   *
   * @see https://www.drupal.org/node/3320855
   */
  public function blockLibraryRedirect() {
  public function blockLibraryRedirect(RouteMatchInterface $route_match, Request $request) {
    @trigger_error('The path /admin/structure/block/block-content is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block. See https://www.drupal.org/node/3320855.', E_USER_DEPRECATED);
    $route = 'entity.block_content.collection';
    $helper = new PathChangedHelper($route_match, $request);
    $params = [
      '%old_path' => Url::fromRoute("$route.bc")->toString(),
      '%new_path' => Url::fromRoute($route)->toString(),
      '%old_path' => $helper->oldPath(),
      '%new_path' => $helper->newPath(),
      '%change_record' => 'https://www.drupal.org/node/3320855',
    ];
    $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params);
    $this->messenger()->addWarning($warning_message);
    $this->getLogger('block_content')
      ->warning('A user was redirected from %old_path to %new_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);
      ->warning('A user was redirected from %old_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);

    return $this->redirect($route, [], [], 301);
    return $helper->redirect();
  }

  /**
   * Provides a redirect to block edit page.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   A route match object, used for the route name and the parameters.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param Drupal\block_content\BlockContentInterface $block_content
   *   The block to be edited.
   *
@@ -203,50 +219,21 @@ public function blockLibraryRedirect() {
   *   /admin/content/block/{block_content} directly instead of
   *   /block/{block_content}.
   *
   * @see https://www.drupal.org/node/2317981
   */
  public function editRedirect(BlockContentInterface $block_content): RedirectResponse {
    @trigger_error('The path /block/{block_content} is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block/{block_content}. See https://www.drupal.org/node/2317981.', E_USER_DEPRECATED);
    $route = 'entity.block_content.edit_form';
    $params = [
      '%old_path' => Url::fromRoute("$route.bc", ['block_content' => $block_content->id()])->toString(),
      '%new_path' => Url::fromRoute($route, ['block_content' => $block_content->id()])->toString(),
      '%change_record' => 'https://www.drupal.org/node/3320855',
    ];
    $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params);
    $this->messenger()->addWarning($warning_message);
    $this->getLogger('block_content')->warning('A user was redirected from %old_path to %new_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);

    return $this->redirect($route, ['block_content' => $block_content->id()], [], 301);
  }

  /**
   * Provides a redirect to block delete page.
   *
   * @param Drupal\block_content\BlockContentInterface $block_content
   *   The block to be deleted.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *
   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use
   *   /admin/content/block/{block_content}/delete directly instead of
   *   /block/{block_content}/delete.
   *
   * @see https://www.drupal.org/node/2317981
   * @see https://www.drupal.org/node/3320855
   */
  public function deleteRedirect(BlockContentInterface $block_content): RedirectResponse {
    @trigger_error('The path /block/{block_content}/delete is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block/{block_content}/delete. See https://www.drupal.org/node/2317981.', E_USER_DEPRECATED);
    $route = 'entity.block_content.delete_form';
  public function editRedirect(RouteMatchInterface $route_match, Request $request, BlockContentInterface $block_content): RedirectResponse {
    @trigger_error('The path /block/{block_content} is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block/{block_content}. See https://www.drupal.org/node/3320855.', E_USER_DEPRECATED);
    $helper = new PathChangedHelper($route_match, $request);
    $params = [
      '%old_path' => Url::fromRoute("$route.bc", ['block_content' => $block_content->id()])->toString(),
      '%new_path' => Url::fromRoute($route, ['block_content' => $block_content->id()])->toString(),
      '%old_path' => $helper->oldPath(),
      '%new_path' => $helper->newPath(),
      '%change_record' => 'https://www.drupal.org/node/3320855',
    ];
    $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params);
    $this->messenger()->addWarning($warning_message);
    $this->getLogger('block_content')->warning('A user was redirected from %old_path to %new_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params);

    return $this->redirect($route, ['block_content' => $block_content->id()], [], 301);
    return $helper->redirect();
  }

}
+237 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\block_content\Routing;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\RouteCollection;

/**
 * Subscriber for Block content BC routes.
 */
class RouteSubscriber extends RouteSubscriberBase {

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The route collection for adding routes.
   *
   * @var \Symfony\Component\Routing\RouteCollection
   */
  protected $collection;

  /**
   * The current base path.
   *
   * @var string
   */
  protected $basePath;

  /**
   * The BC base path.
   *
   * @var string
   */
  protected $basePathBc;

  /**
   * The redirect controller.
   *
   * @var string
   */
  protected $controller;

  /**
   * Constructs a RouteSubscriber object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
    $this->entityTypeManager = $entity_type_manager;
    $this->moduleHandler = $module_handler;
  }

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    $this->collection = $collection;

    // @see block_content.routing.yml
    if ($this->setUpBaseRoute('entity.block_content_type.collection')) {
      $this->addRedirectRoute('block_content.type_add');
    }

    $entity_type = $this->entityTypeManager->getDefinition('block_content');
    if ($this->setUpBaseRoute($entity_type->get('field_ui_base_route'))) {
      foreach ($this->childRoutes($entity_type) as $route_name) {
        $this->addRedirectRoute($route_name);
      }
    }
  }

  /**
   * Gets parameters from a base route and saves them in class variables.
   *
   * @param string $base_route_name
   *   The name of a base route that already has a BC variant.
   *
   * @return bool
   *   TRUE if all parameters are set, FALSE if not.
   */
  protected function setUpBaseRoute(string $base_route_name): bool {
    $base_route = $this->collection->get($base_route_name);
    $base_route_bc = $this->collection->get("$base_route_name.bc");
    if (empty($base_route) || empty($base_route_bc)) {
      return FALSE;
    }

    $this->basePath = $base_route->getPath();
    $this->basePathBc = $base_route_bc->getPath();
    $this->controller = $base_route_bc->getDefault('_controller');
    if (empty($this->basePath) || empty($this->basePathBc) || empty($this->controller) || $this->basePathBc === $this->basePath) {
      return FALSE;
    }

    return TRUE;
  }

  /**
   * Adds a redirect route.
   *
   * @param string $route_name
   *   The name of a route whose path has changed.
   */
  protected function addRedirectRoute(string $route_name): void {
    // Exit early if the BC route is already there.
    if (!empty($this->collection->get("$route_name.bc"))) {
      return;
    }

    $route = $this->collection->get($route_name);
    if (empty($route)) {
      return;
    }

    $new_path = $route->getPath();
    if (!str_starts_with($new_path, $this->basePath)) {
      return;
    }

    $bc_route = clone $route;
    // Set the path to what it was in earlier versions of Drupal.
    $bc_route->setPath($this->basePathBc . substr($new_path, strlen($this->basePath)));
    if ($bc_route->getPath() === $route->getPath()) {
      return;
    }

    // Replace the handler with the stored redirect controller.
    $defaults = array_diff_key($route->getDefaults(), array_flip([
      '_entity_form',
      '_entity_list',
      '_entity_view',
      '_form',
    ]));
    $defaults['_controller'] = $this->controller;
    $bc_route->setDefaults($defaults);

    $this->collection->add("$route_name.bc", $bc_route);
  }

  /**
   * Creates a list of routes that need BC redirects.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   *
   * @return string[]
   *   A list of route names.
   */
  protected function childRoutes(EntityTypeInterface $entity_type): array {
    $route_names = [];

    if ($field_ui_base_route = $entity_type->get('field_ui_base_route')) {
      $updated_routes = new RouteCollection();
      $updated_routes->add($field_ui_base_route, $this->collection->get($field_ui_base_route));
      $event = new RouteBuildEvent($updated_routes);

      // Apply route subscribers that add routes based on field_ui_base_route,
      // in the order of their weights.
      $subscribers = [
        'field_ui' => 'field_ui.subscriber',
        'content_translation' => 'content_translation.subscriber',
      ];
      foreach ($subscribers as $module_name => $service_name) {
        if ($this->moduleHandler->moduleExists($module_name)) {
          \Drupal::service($service_name)->onAlterRoutes($event);
        }
      }

      $updated_routes->remove($field_ui_base_route);
      $route_names = array_merge($route_names, array_keys($updated_routes->all()));
      $route_names = array_merge($route_names, [
        // @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes()
        "config_translation.item.add.{$field_ui_base_route}",
        "config_translation.item.edit.{$field_ui_base_route}",
        "config_translation.item.delete.{$field_ui_base_route}",
      ]);
    }

    if ($entity_type_id = $entity_type->getBundleEntityType()) {
      $route_names = array_merge($route_names, [
        // @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getRoutes()
        "entity.{$entity_type_id}.delete_form",
        // @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes()
        "entity.{$entity_type_id}.config_translation_overview",
        // @see \Drupal\user\Entity\EntityPermissionsRouteProvider::getRoutes()
        "entity.{$entity_type_id}.entity_permissions_form",
      ]);
    }

    if ($entity_id = $entity_type->id()) {
      $route_names = array_merge($route_names, [
        // @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes()
        "entity.field_config.config_translation_overview.{$entity_id}",
        "config_translation.item.add.entity.field_config.{$entity_id}_field_edit_form",
        "config_translation.item.edit.entity.field_config.{$entity_id}_field_edit_form",
        "config_translation.item.delete.entity.field_config.{$entity_id}_field_edit_form",
        // @see \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage::buildRoutes()
        "layout_builder.defaults.{$entity_id}.disable",
        "layout_builder.defaults.{$entity_id}.discard_changes",
        "layout_builder.defaults.{$entity_id}.view",
      ]);
    }

    return $route_names;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    $events = parent::getSubscribedEvents();
    // Go after ContentTranslationRouteSubscriber.
    $events[RoutingEvents::ALTER] = ['onAlterRoutes', -300];
    return $events;
  }

}
Loading