Node Swapper is a simple, admin-facing tool that facilitates the otherwise rote job of replacing one node (and all of its requisite aliases and redirects) with another. The use-case is simple: an admin has a popular webpage that they want to replace with a new page without losing traffic or messing with aliases and redirects by-hand. Perhaps the new page uses a new node type, or perhaps the replacement page uses a new UI pattern. Either way, Node Swapper will cut out the guess work and make the swap a success.

The admin need only build the new page, publish it under a temporary alias, and then use Node Swapper, which exists as a UI tool located under Admin > Configuration > System > Node Swapper.

Node Swapper takes care of mapping all the existing page's redirects and aliases to the new node. Before undergoing the swap, admins are presented with a confirmation page that details what exactly Node Swapper will do. Admins also have the option to unpublish the old node and choose a custom suffix to append at the end of the old node's alias.

Adding a suffix onto the old node alias ensures that:

  1. The old node still lives at a familiar location
  2. The new node can successfully inherit the old alias

Admins are also free to set a default alias suffix and a default setting for whether to unpublish the old node at Admin > Configuration > System > Node Swapper Settings.

Similar Projects

This project leans heavily on the API from the Redirect and Alias projects and, in a sense, can be thought of as an extension of those existing modules. It doesn't really add any new features to those existing APIs, it just makes the existing ones easier to use for non-technical users, and it makes site management easier for technical users who want to save time.

That said, I don't know of an existing module that does this exact job.

Project link

https://www.drupal.org/project/node_swapper/

Comments

abenbow created an issue. See original summary.

vishal.kadam’s picture

Issue summary: View changes
avpaderno’s picture

Thank you for applying!

Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.

The important notes are the following.

  • If you have not done it yet, enable GitLab CI for the project, and fix what reported from the phpcs job. This help to fix most of what reviewers would report.
  • For the time this application is open, only your commits are allowed. No other people, including other maintainers/co-maintainers can make commits.
  • The purpose of this application is giving you a new drupal.org role that allows you to opt projects into security advisory coverage, either projects you already created, or projects you will create. The project status won't be changed by this application.
  • Nobody else will get the permission to opt projects into security advisory policy. If there are other maintainers/co-maintainers who will to get that permission, they need to apply with a different module.
  • We only accept an application per user. If you change your mind about the project to use for this application, or it is necessary to use a different project for the application, please update the issue summary with the link to the correct project and the issue title with the project name and the branch to review.

To the reviewers

Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.

The important notes are the following.

  • It is preferable to wait for a Code Review Administrator before commenting on newly created applications. Code Review Administrators will do some preliminary checks that are necessary before any change on the project files is suggested.
  • Reviewers should show the output of a CLI tool only once per application. The configuration used for these tools needs to be the same configuration used by GitLab CI, stored in the GitLab Templates repository.
  • It may be best to have the applicant fix things before further review.

For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.

vishal.kadam’s picture

Status: Needs review » Needs work

1. FILE: README.md

The README file is missing the required sections - Installation and Configuration.

2. FILE: node_swapper.info.yml

version: '8.x-1.2.0'
project: 'node_swapper'

Remove "project" and "version" from the info file, it will be added by drupal.org packaging automatically.

abenbow’s picture

Addressed #4 in 4b7eb528

abenbow’s picture

Status: Needs work » Needs review
bbu23’s picture

The module is missing the composer.json file.

Use dependency injection for loading Path Aliases and Nodes

FILE: /var/www/html/web/modules/custom/node_swapper/src/Service/NodeSwapperService.php
------------------------------------------------------------------------------------------------------
FOUND 0 ERRORS AND 4 WARNINGS AFFECTING 4 LINES
------------------------------------------------------------------------------------------------------
  90 | WARNING | PathAlias::load calls should be avoided in classes, use dependency injection instead
  97 | WARNING | PathAlias::load calls should be avoided in classes, use dependency injection instead
 124 | WARNING | Node::load calls should be avoided in classes, use dependency injection instead
 125 | WARNING | Node::load calls should be avoided in classes, use dependency injection instead
------------------------------------------------------------------------------------------------------

And actually, you should use ParamConverter in routing file to get node objects directly instead of loading them in the buildForm method. You can get inspired from other node route definitions.

In src/Form/NodeSwapperConfirmForm.php line 88, the param type for $entity_type_manager should be \Drupal\Core\Entity\EntityTypeManagerInterface, though the __construct() method does not require any function description anymore, so unless you want to keep it, the whole description block can be removed.

$src_node->title->value should be $src_node->label(), same for destination node.

bbu23’s picture

Status: Needs review » Needs work
bbu23’s picture

And just noticed that your default branch is main. That should not be the case, it should probably be 1.2.x for your latest tag release.
See https://www.drupal.org/docs/develop/git/git-for-drupal-project-maintaine...

abenbow’s picture

I've made some updates to Node Swapper in accordance with bbu23's requests.

  • New composer.json file with maintainer, support, license, and meta information
  • Removed Node::Load and PathAlias::load calls and replaced them with dependency injection
  • Used ParamConverter to transform route parameters directly into entities in the ConfirmForm class
  • Changed the way titles are accessed
  • Changed the main branch to 1.2.x

Question: now that the main branch is 1.2.x, should I update this issue's description to include that information?

abenbow’s picture

Status: Needs work » Needs review
bbu23’s picture

Status: Needs review » Needs work

Thanks! 👍 Changes look good to me, except for the inheritdoc comment on NodeSwapperConfirmForm constructor. It should be completely removed or defined, not inherited (this is super minor).

And I also noticed that the config schema defines the old_node_unpublish as boolean, but the default value from the config install folder is set to integer. It should match the defined type.

Changing the status because of the schema issue.

Question: now that the main branch is 1.2.x, should I update this issue's description to include that information?

You didn't have the main branch mentioned in the description, so that's up to you. What's important is that the default gitlab branch is set to 1.2.x as expected.

abenbow’s picture

Status: Needs work » Needs review

Thanks for the feedback!

I made some new commits, and this is what is different:

  • No more inheritdoc on any construct methods (I figure consistency is key)
  • Updated the default config to install a boolean value. I also changed the way the NodeSwapperService works to coerce values into bools, just so it is a little more true to what the configuration is expecting out of the API
bbu23’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me!
Let's wait for others / Project Moderator to take a look.

avpaderno’s picture

Status: Reviewed & tested by the community » Needs work
  • The following points are just a start and don't necessarily encompass all of the changes that may be necessary
  • A specific point may just be an example and may apply in other places
  • A review is about code that does not follow the coding standards, contains possible security issue, or does not correctly use the Drupal API
  • The single review points are not ordered, not even by importance

src/Form/NodeSwapperConfirmForm.php

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

A parent class already defines that property, which does not need to be re-declared.

  public function __construct(
    ConfigFactoryInterface $config_factory,
    RequestStack $request_stack,
    Connection $connection,
    AliasManagerInterface $alias_manager,
    TimeInterface $time,
    EntityTypeManagerInterface $entity_type_manager,
    NodeSwapperService $node_swapper_service,
    NodeInterface $src,
    NodeInterface $dest,
  ) {
    $this->configFactory = $config_factory;
    $this->requestStack = $request_stack;
    $this->database = $connection;
    $this->aliasManager = $alias_manager;
    $this->time = $time;
    $this->entityTypeManager = $entity_type_manager;
    $this->nodeSwapperService = $node_swapper_service;
    $this->src = $src;
    $this->dest = $dest;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('request_stack'),
      $container->get('database'),
      $container->get('path_alias.manager'),
      $container->get('datetime.time'),
      $container->get('entity_type.manager'),
      $container->get('node_swapper.service'),
      $container->get('current_route_match')->getParameter('src'),s
      $container->get('current_route_match')->getParameter('dest'),
    );
  }

create() should also call $this->setMessenger() with the messenger service retrieved from the container.

    $message = '';
    $message .= "<p>This will point all alises and redirects of \"{$src_node->label()}\" to \"{$dest_node->label()}\".</p>";
    $message .= "<p>In addition, <b>ALL</b> aliases of \"New Node\" will be deleted. Redirects will not be deleted.</p>";

$message needs to be a translatable string.

abenbow’s picture

Status: Needs work » Needs review

Resolved the requests in #15 in a new commit: here

avpaderno’s picture

Let's see if other reviewers report other changes.

avpaderno’s picture

Status: Needs review » Needs work

src/Form/NodeSwapperConfirmForm.php

  public function __construct(
    ConfigFactoryInterface $config_factory,
    RequestStack $request_stack,
    Connection $connection,
    AliasManagerInterface $alias_manager,
    TimeInterface $time,
    EntityTypeManagerInterface $entity_type_manager,
    NodeSwapperService $node_swapper_service,
    NodeInterface $src,
    NodeInterface $dest,
    MessengerInterface $messenger,
  ) {
    $this->configFactory = $config_factory;
    $this->requestStack = $request_stack;
    $this->database = $connection;
    $this->aliasManager = $alias_manager;
    $this->time = $time;
    $this->entityTypeManager = $entity_type_manager;
    $this->nodeSwapperService = $node_swapper_service;
    $this->src = $src;
    $this->dest = $dest;
    $this->setMessenger($messenger);
  }

Starting with Drupal 10.2, the parent class requires also the $typedConfigManager parameter, which also means the project cannot be compatible with all the Drupal 10 releases and Drupal 11. It needs to at least require Drupal 10.2, and pass the required parameter to the parent class's constructor.

src/Form/NodeSwapperForm.php

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

The parent class already defines that property. It also define a method to set it.

src/Form/NodeSwapperSettingsForm.php

    $form['old_node_suffix'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Old Node Suffix'),
      '#description' => $this->t('The default alphanumeric ending to append on old URLs. Ex: /my-url-[SUFFIX]. Users can override this setting on a case-by-case basis.'),
      '#required' => TRUE,
      '#default_value' => $this->config('node_swapper.settings')->get('old_node_suffix'),
      '#pattern' => '[a-z0-9-_]+',
    ];

With Drupal 10 and Drupal 11, there is no longer need to use #default_value for each form element, when the parent class is ConfigFormBase: It is sufficient to use #config_target, as in the following code.

    $form['image_toolkit'] = [
      '#type' => 'radios',
      '#title' => $this->t('Select an image processing toolkit'),
      '#config_target' => 'system.image:toolkit',
      '#options' => [],
    ];

Using that code, it is no longer needed to save the configuration values in the form submission handler: The parent class will take care of that.

abenbow’s picture

Status: Needs work » Needs review

I've made some updates in the latest commit that address #18

Unrelated, but I've added a langcode onto the config ymls since Drupal requested that that should be done.

Drupal Core reqs
I've updated the module to require 10.2.x || ^11

src/Form/NodeSwapperConfirmForm.php
So it looks like with regards to the $typedConfigManager parameter, that that only applies to instances of ConfigFormBase, at least according to the official notice. Since this is an instance of ConfirmFormBase, I've left it alone.

src/Form/NodeSwapperForm.php
I removed the unneeded $configFactory initialization, instead just calling parent::setConfigFactory in the constructor.

src/Form/NodeSwapperSettingsForm.php
I removed the calls to the configFactory when setting a default value, opting instead to use the #config_target key. This meant that I could also drop the submitForm method entirely.

avpaderno’s picture

Status: Needs review » Needs work

I apologize: When I added two sentences, I added them in the wrong place and accidently removed the existing sentences. The correct comment was supposed to be the following one.

src/Form/NodeSwapperConfirmForm.php

  public function __construct(
    ConfigFactoryInterface $config_factory,
    RequestStack $request_stack,
    Connection $connection,
    AliasManagerInterface $alias_manager,
    TimeInterface $time,
    EntityTypeManagerInterface $entity_type_manager,
    NodeSwapperService $node_swapper_service,
    NodeInterface $src,
    NodeInterface $dest,
    MessengerInterface $messenger,
  ) {
    $this->configFactory = $config_factory;
    $this->requestStack = $request_stack;
    $this->database = $connection;
    $this->aliasManager = $alias_manager;
    $this->time = $time;
    $this->entityTypeManager = $entity_type_manager;
    $this->nodeSwapperService = $node_swapper_service;
    $this->src = $src;
    $this->dest = $dest;
    $this->setMessenger($messenger);
  }

Some of those properties are already defined from a parent class; they do not need to be defined again. The parent class also defines methods to set those properties; those methods should be used in the constructor.

  • $configFactory: setConfigFactory()
  • $requestStack: setRequestStack()

The documentation for ConfirmFormBase also lists the methods to get the value of those properties.

src/Form/NodeSwapperForm.php

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

The parent class already defines that property. It also define a method to set it.

src/Form/NodeSwapperSettingsForm.php

Starting with Drupal 10.2, the parent class requires also the $typedConfigManager parameter, which also means the project cannot be compatible with all the Drupal 10 releases and Drupal 11. It needs to at least require Drupal 10.2, and pass the required parameter to the parent class's constructor.

With Drupal 10 and Drupal 11, there is no longer need to use #default_value for each form element, when the parent class is ConfigFormBase: It is sufficient to use #config_target, as in the following code.

    $form['image_toolkit'] = [
      '#type' => 'radios',
      '#title' => $this->t('Select an image processing toolkit'),
      '#config_target' => 'system.image:toolkit',
      '#options' => [],
    ];

Using that code, it is no longer needed to save the configuration values in the form submission handler: The parent class will take care of that.

Yes, the form submission handler is no longer necessary, since the parent class implements it and saves the configuration values basing on the values used for #config_target.

Furthermore, in addition to what reported for the src/Form/NodeSwapperForm.php file, the following code needs to use configFactory().

    $config = $this->configFactory->get('node_swapper.settings');

    $form['unpublish'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Unpublish Old Node'),
      '#description' => $this->t('If checked, the old node will be unpublished'),
      '#default_value' => $config->get('old_node_unpublish'),
    ];
vishal.kadam’s picture

Priority: Normal » Minor

I am changing priority as per Issue priorities.

avpaderno’s picture

This thread has been idle, in the needs work state with no activity for some months.

May you confirm you are still pursuing this application? If this is the case, and you made commits basing on what previously reported, or you can answer the questions previously asked, please change the status to Needs review.

abenbow’s picture

Hey, my apologies for the delay. I've not had the time to sit down and think through the fixes in #20 yet.

I'm going to schedule some time this week to resolve the most recent requests. Thank you for your patience!

abenbow’s picture

Status: Needs work » Needs review

I've made some new updates to the Node Swapper package.

Both the NodeSwapperConfirmForm and the NodeSwapperSettingsForm use less dependency injection now and instead use parent class methods in order to access the configFactory and the requestStack.

I've also update the composer.json and info file so that the package is using newer dependencies and is prepared for use across 10 and 11.

Here is the source code:
https://git.drupalcode.org/project/node_swapper

rushikesh raval’s picture

Priority: Minor » Critical

I am changing priority as per Issue priorities.

avpaderno’s picture

Title: [1.x] Node Swapper » [1.2.x] Node Swapper
Status: Needs review » Needs work

src/Form/NodeSwapperConfirmForm.php

Code compatible with latest Drupal releases should use property promotion.

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('database'),
      $container->get('path_alias.manager'),
      $container->get('datetime.time'),
      $container->get('entity_type.manager'),
      $container->get('node_swapper.service'),
      $container->get('current_route_match')->getParameter('src'),
      $container->get('current_route_match')->getParameter('dest'),
      $container->get('messenger'),
    );
  }

  /**
   * ID of the item to swap from.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $src;

  /**
   * ID of the item to swap to.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $dest;

Properties are expected to be defined before any method.

avpaderno’s picture

Priority: Critical » Normal
abenbow’s picture

Status: Needs work » Needs review

Resolved on this new commit: 4b46cbbb185c2d77c711db65d3c66e560add9c36

Changes:

  • Re-arranged properties so they come before methods
  • Alphabetized methods and properties to just make the files a bit easier to navigate.
avpaderno’s picture

Status: Needs review » Needs work

src/Form/NodeSwapperConfirmForm.php

  public function __construct(
    Connection $connection,
    AliasManagerInterface $alias_manager,
    TimeInterface $time,
    EntityTypeManagerInterface $entity_type_manager,
    NodeSwapperService $node_swapper_service,
    NodeInterface $src,
    NodeInterface $dest,
    MessengerInterface $messenger,
  ) {
    $this->database = $connection;
    $this->aliasManager = $alias_manager;
    $this->time = $time;
    $this->entityTypeManager = $entity_type_manager;
    $this->nodeSwapperService = $node_swapper_service;
    $this->src = $src;
    $this->dest = $dest;
    $this->setMessenger($messenger);
  }

Code compatible with latest Drupal releases should use property promotion.

The constructor should call ::setConfigFactory(), in the same way it calls ::setMessenger(). It should also initialize ::$requestStack for which, unfortunately, the base class does not provide a method.

node_swapper.services.yml

Services can be autowired starting with Drupal 9.3.x. This means that it is no longer necessary to give a list of service arguments in the .services.yml file; they will be retrieved from the constructor definition.

vishal.kadam’s picture

Priority: Normal » Minor

I am changing priority as per Issue priorities.

abenbow’s picture

Status: Needs work » Closed (won't fix)

I would like to formally close this application. I haven't had the time to work on this that I would have hoped for, so to honor your time, I would like to take the job of reviewing this module off or your plates.

I appreciate the help the reviewers have provided to me during the course of this application. Thank you! And once I have more time, I may apply at some other (as yet undetermined) date.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.