Commit 2df0fc02 authored by catch's avatar catch

Issue #2850085 by maxocub, heddn, Jo Fitzgerald, Gábor Hojtsy, catch,...

Issue #2850085 by maxocub, heddn, Jo Fitzgerald, Gábor Hojtsy, catch, larowlan, mikeryan: Redirects for translation set migration path in Drupal 6 and 7
parent 8685e7c7
......@@ -96,9 +96,12 @@ public function convert(array $defaults) {
// If a converter returns NULL it means that the parameter could not be
// converted.
$defaults[$name] = $this->getConverter($definition['converter'])->convert($defaults[$name], $definition, $name, $defaults);
$value = $defaults[$name];
$defaults[$name] = $this->getConverter($definition['converter'])->convert($value, $definition, $name, $defaults);
if (!isset($defaults[$name])) {
throw new ParamNotConvertedException(sprintf('The "%s" parameter was not converted for the path "%s" (route name: "%s")', $name, $route->getPath(), $defaults[RouteObjectInterface::ROUTE_NAME]));
$message = 'The "%s" parameter was not converted for the path "%s" (route name: "%s")';
$route_name = $defaults[RouteObjectInterface::ROUTE_NAME];
throw new ParamNotConvertedException(sprintf($message, $name, $route->getPath(), $route_name), 0, NULL, $route_name, [$name => $value]);
}
}
......
......@@ -7,4 +7,58 @@
*/
class ParamNotConvertedException extends \Exception {
/**
* The route name that was not converted.
*
* @var string
*/
protected $routeName = "";
/**
* The raw parameters that were not converted.
*
* @var array
*/
protected $rawParameters = [];
/**
* Constructs the ParamNotConvertedException.
*
* @param string $message
* The Exception message to throw.
* @param int $code
* The Exception code.
* @param \Exception $previous
* The previous exception used for the exception chaining.
* @param string $route_name
* The route name that was not converted.
* @param array $raw_parameters
* The raw parameters that were not converted.
*/
public function __construct($message = "", $code = 0, \Exception $previous = NULL, $route_name = "", array $raw_parameters = []) {
parent::__construct($message, $code, $previous);
$this->routeName = $route_name;
$this->rawParameters = $raw_parameters;
}
/**
* Get the route name that was not converted.
*
* @return string
* The route name that was not converted.
*/
public function getRouteName() {
return $this->routeName;
}
/**
* Get the raw parameters that were not converted.
*
* @return array
* The raw parameters that were not converted.
*/
public function getRawParameters() {
return $this->rawParameters;
}
}
<?php
namespace Drupal\node\EventSubscriber;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Redirect node translations that have been consolidated by migration.
*
* If we migrated node translations from Drupal 6 or 7, these nodes are now
* combined with their source language node. Since there still might be
* references to the URLs of these now consolidated nodes, this service catches
* the 404s and try to redirect them to the right node in the right language.
*
* The mapping of the old nids to the new ones is made by the
* NodeTranslationMigrateSubscriber class during the migration and is stored
* in the "node_translation_redirect" key/value collection.
*
* @see \Drupal\node\NodeServiceProvider
* @see \Drupal\node\EventSubscriber\NodeTranslationMigrateSubscriber
*/
class NodeTranslationExceptionSubscriber implements EventSubscriberInterface {
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValue;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs the NodeTranslationExceptionSubscriber.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key value factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(KeyValueFactoryInterface $key_value, LanguageManagerInterface $language_manager, UrlGeneratorInterface $url_generator, StateInterface $state) {
$this->keyValue = $key_value;
$this->languageManager = $language_manager;
$this->urlGenerator = $url_generator;
$this->state = $state;
}
/**
* Redirects not found node translations using the key value collection.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The exception event.
*/
public function onException(GetResponseForExceptionEvent $event) {
$exception = $event->getException();
// If this is not a 404, we don't need to check for a redirection.
if (!($exception instanceof NotFoundHttpException)) {
return;
}
$previous_exception = $exception->getPrevious();
if ($previous_exception instanceof ParamNotConvertedException) {
$route_name = $previous_exception->getRouteName();
$parameters = $previous_exception->getRawParameters();
if ($route_name === 'entity.node.canonical' && isset($parameters['node'])) {
// If the node_translation_redirect state is not set, we don't need to check
// for a redirection.
if (!$this->state->get('node_translation_redirect')) {
return;
}
$old_nid = $parameters['node'];
$collection = $this->keyValue->get('node_translation_redirect');
if ($old_nid && $value = $collection->get($old_nid)) {
list($nid, $langcode) = $value;
$language = $this->languageManager->getLanguage($langcode);
$url = $this->urlGenerator->generateFromRoute('entity.node.canonical', ['node' => $nid], ['language' => $language]);
$response = new RedirectResponse($url, 301);
$event->setResponse($response);
}
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
$events[KernelEvents::EXCEPTION] = ['onException'];
return $events;
}
}
<?php
namespace Drupal\node\EventSubscriber;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Event\EventBase;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Creates a key value collection for migrated node translation redirections.
*
* If we are migrating node translations from Drupal 6 or 7, these nodes will be
* combined with their source node. Since there still might be references to the
* URLs of these now consolidated nodes, this service saves the mapping between
* the old nids to the new ones to be able to redirect them to the right node in
* the right language.
*
* The mapping is stored in the "node_translation_redirect" key/value collection
* and the redirection is made by the NodeTranslationExceptionSubscriber class.
*
* @see \Drupal\node\NodeServiceProvider
* @see \Drupal\node\EventSubscriber\NodeTranslationExceptionSubscriber
*/
class NodeTranslationMigrateSubscriber implements EventSubscriberInterface {
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValue;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs the NodeTranslationMigrateSubscriber.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key value factory.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(KeyValueFactoryInterface $key_value, StateInterface $state) {
$this->keyValue = $key_value;
$this->state = $state;
}
/**
* Helper method to check if we are migrating translated nodes.
*
* @param \Drupal\migrate\Event\EventBase $event
* The migrate event.
*
* @return bool
* True if we are migrating translated nodes, false otherwise.
*/
protected function isNodeTranslationsMigration(EventBase $event) {
$migration = $event->getMigration();
$source_configuration = $migration->getSourceConfiguration();
$destination_configuration = $migration->getDestinationConfiguration();
return !empty($source_configuration['translations']) && $destination_configuration['plugin'] === 'entity:node';
}
/**
* Maps the old nid to the new one in the key value collection.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The migrate post row save event.
*/
public function onPostRowSave(MigratePostRowSaveEvent $event) {
if ($this->isNodeTranslationsMigration($event)) {
$row = $event->getRow();
$source = $row->getSource();
$destination = $row->getDestination();
$collection = $this->keyValue->get('node_translation_redirect');
$collection->set($source['nid'], [$destination['nid'], $destination['langcode']]);
}
}
/**
* Set the node_translation_redirect state to enable the redirections.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migrate import event.
*/
public function onPostImport(MigrateImportEvent $event) {
if ($this->isNodeTranslationsMigration($event)) {
$this->state->set('node_translation_redirect', TRUE);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
$events[MigrateEvents::POST_ROW_SAVE] = ['onPostRowSave'];
$events[MigrateEvents::POST_IMPORT] = ['onPostImport'];
return $events;
}
}
<?php
namespace Drupal\node;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\node\EventSubscriber\NodeTranslationExceptionSubscriber;
use Drupal\node\EventSubscriber\NodeTranslationMigrateSubscriber;
use Symfony\Component\DependencyInjection\Reference;
/**
* Registers services in the container.
*/
class NodeServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
// Register the node.node_translation_migrate service in the container if
// the migrate and language modules are enabled.
$modules = $container->getParameter('container.modules');
if (isset($modules['migrate']) && isset($modules['language'])) {
$container->register('node.node_translation_migrate', NodeTranslationMigrateSubscriber::class)
->addTag('event_subscriber')
->addArgument(new Reference('keyvalue'))
->addArgument(new Reference('state'));
}
// Register the node.node_translation_exception service in the container if
// the language module is enabled.
if (isset($modules['language'])) {
$container->register('node.node_translation_exception', NodeTranslationExceptionSubscriber::class)
->addTag('event_subscriber')
->addArgument(new Reference('keyvalue'))
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('url_generator'))
->addArgument(new Reference('state'));
}
}
}
<?php
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests node translation redirections.
*
* @group migrate_drupal
* @group node
*/
class NodeTranslationRedirectTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'language',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(['node']);
$this->installSchema('node', ['node_access']);
$this->installSchema('system', ['key_value']);
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->executeMigrations([
'language',
'd6_language_types',
'd6_language_negotiation_settings',
'd6_node_settings',
'd6_node',
'd6_node_translation',
]);
}
/**
* Tests that not found node translations are redirected.
*/
public function testNodeTranslationRedirect() {
$kernel = $this->container->get('http_kernel');
$request = Request::create('/node/11');
$response = $kernel->handle($request);
$this->assertSame(301, $response->getStatusCode());
$this->assertSame('/node/10', $response->getTargetUrl());
}
}
<?php
namespace Drupal\Tests\node\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests node translation redirections.
*
* @group migrate_drupal
* @group node
*/
class NodeTranslationRedirectTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'language',
'menu_ui',
'node',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig('node');
$this->installSchema('node', ['node_access']);
$this->installSchema('system', ['key_value']);
$this->executeMigrations([
'language',
'd7_language_types',
'd7_language_negotiation_settings',
'd7_user_role',
'd7_user',
'd7_node_type',
'd7_node',
'd7_node_translation',
]);
}
/**
* Tests that not found node translations are redirected.
*/
public function testNodeTranslationRedirect() {
$kernel = $this->container->get('http_kernel');
$request = Request::create('/node/3');
$response = $kernel->handle($request);
$this->assertSame(301, $response->getStatusCode());
$this->assertSame('/node/2', $response->getTargetUrl());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment