From ea41701624d46edb8fc36f21bf3b09d5368c6973 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20B=C3=BCrk?= <stefan@buerk.tech>
Date: Thu, 10 Nov 2022 13:11:07 +0100
Subject: [PATCH] [FEATURE] Add two PSR-14 events around auto create redirects

With #91776 auto-created redirects have been stored with a
pid of the page, which the slug has been changed. This was
streamlined and changed to use the site rootPageId instead
with #99044, which reverted the original use-case.

This change implements two new PSR-14 events around each
auto-create-redirect:

* `ModifyAutoCreateRedirectRecordBeforePersistingEvent`:
   can be used to modify the redirect record before it is
   persisted

* `AfterAutoCreateRedirectHasBeenPersistedEvent`:
   can be used to chain further task after the redirect
   has been persisted to the database. It's kind of a
   plain notification event.

Resolves: #99834
Related: #99802
Related: #99044
Related: #91776
Releases: main
Change-Id: I974020b148a3b5eb109624648016a79ed557af3e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76539
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Sybille Peters <sypets@gmx.de>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 ...utoCreateRedirectHasBeenPersistedEvent.rst |  74 ++++++++++
 ...ateRedirectRecordBeforePersistingEvent.rst |  85 ++++++++++++
 ...utoCreateRedirectHasBeenPersistedEvent.php |  54 ++++++++
 ...ateRedirectRecordBeforePersistingEvent.php |  59 ++++++++
 .../redirects/Classes/Service/SlugService.php |  21 ++-
 ...utoCreateRedirectHasBeenPersistedEvent.csv |   4 +
 ...ateRedirectRecordBeforePersistingEvent.csv |   4 +
 .../Functional/Service/SlugServiceTest.php    | 126 +++++++++++++++++-
 ...reateRedirectHasBeenPersistedEventTest.php |  61 +++++++++
 ...edirectRecordBeforePersistingEventTest.php |  97 ++++++++++++++
 10 files changed, 576 insertions(+), 9 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14AfterAutoCreateRedirectHasBeenPersistedEvent.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14ModifyAutoCreateRedirectRecordBeforePersistingEvent.rst
 create mode 100644 typo3/sysext/redirects/Classes/Event/AfterAutoCreateRedirectHasBeenPersistedEvent.php
 create mode 100644 typo3/sysext/redirects/Classes/Event/ModifyAutoCreateRedirectRecordBeforePersistingEvent.php
 create mode 100644 typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_AfterAutoCreateRedirectHasBeenPersistedEvent.csv
 create mode 100644 typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_ModifyAutoCreateRedirectRecordBeforePersistingEvent.csv
 create mode 100644 typo3/sysext/redirects/Tests/Unit/Event/AfterAutoCreateRedirectHasBeenPersistedEventTest.php
 create mode 100644 typo3/sysext/redirects/Tests/Unit/Event/ModifyAutoCreateRedirectRecordBeforePersistingEventTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14AfterAutoCreateRedirectHasBeenPersistedEvent.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14AfterAutoCreateRedirectHasBeenPersistedEvent.rst
new file mode 100644
index 000000000000..092e43ff6f34
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14AfterAutoCreateRedirectHasBeenPersistedEvent.rst
@@ -0,0 +1,74 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99834-1675612921:
+
+=========================================================================
+Feature: #99834 - New PSR-14 AfterAutoCreateRedirectHasBeenPersistedEvent
+=========================================================================
+
+See :issue:`99834`
+
+Description
+===========
+
+A new PSR-14 event :php:`\TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent`
+is introduced, allowing extension authors to react on persisted auto-created redirects. This
+can be used to call external API or do other tasks based on the real persisted redirects.
+
+..  note::
+
+    To handle later updates or react on manual created redirects in the backend
+    module, available hooks of :php:`\TYPO3\CMS\Core\DataHandling\DataHandler`
+    can be used.
+
+Example:
+--------
+
+Registration of the event listener:
+
+..  code-block:: yaml
+    :caption: EXT:my_extension/Configuration/Services.yaml
+
+    MyVendor\MyExtension\Backend\MyEventListener:
+      tags:
+        - name: event.listener
+          identifier: 'my-extension/after-auto-create-redirect-has-been-persisted'
+
+The corresponding event listener class:
+
+..  code-block:: php
+    :caption: EXT:my_extension/Classes/Backend/MyEventListener.php
+
+    namespace MyVendor\MyExtension\Backend;
+
+    use TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent;
+    use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
+
+    class MyEventListener {
+
+        public function __invoke(
+            AfterAutoCreateRedirectHasBeenPersistedEvent $event
+        ): void {
+            $redirectUid = $event->getRedirectRecord()['uid'] ?? null;
+            if ($redirectUid === null
+                && !($event->getSource() instanceof PlainSlugReplacementRedirectSource)
+            ) {
+                return;
+            }
+
+            // Implement code what should be done with this information. E.g.
+            // write to another table, call a rest api or similar. Find your
+            // use-case.
+        }
+    }
+
+
+Impact
+======
+
+With the new :php:`AfterAutoCreateRedirectHasBeenPersistedEvent`, it's now possible
+to react on persisted auto-created redirects. Manually created redirects can be handled
+by using one of the available :php:`\TYPO3\CMS\Core\DataHandling\DataHandler` hooks,
+not suitable for auto-created redirects.
+
+.. index:: PHP-API, ext:redirects
diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14ModifyAutoCreateRedirectRecordBeforePersistingEvent.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14ModifyAutoCreateRedirectRecordBeforePersistingEvent.rst
new file mode 100644
index 000000000000..dff7b008477c
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-99834-NewPSR-14ModifyAutoCreateRedirectRecordBeforePersistingEvent.rst
@@ -0,0 +1,85 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99834-1675612872:
+
+================================================================================
+Feature: #99834 - New PSR-14 ModifyAutoCreateRedirectRecordBeforePersistingEvent
+================================================================================
+
+See :issue:`99834`
+
+Description
+===========
+
+A new PSR-14 :php:`\TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent`
+is introduced, allowing extension authors to modify the redirect record before it is persisted to
+the database. This can be used to change values based on circumstances like e.g.
+different sub tree settings, not covered by core site configuration. Another use-case
+could be to write data to additional :sql:`sys_redirect` columns added by a custom
+extension for later use.
+
+..  note::
+
+    To handle later updates or react on manually created redirects in the backend
+    module, available hooks of :php:`\TYPO3\CMS\Core\DataHandling\DataHandler`
+    can be used.
+
+Example:
+--------
+
+..  code-block:: yaml
+    :caption: my_extension/Configuration/Services.yaml
+
+    MyVendor\MyExtension\Backend\MyEventListener:
+      tags:
+        - name: event.listener
+          identifier: 'my-extension/after-auto-create-redirect-has-been-persisted'
+
+The corresponding event listener class:
+
+..  code-block:: php
+    :caption: my_extension/Classes/Backend/MyEventListener.php
+
+    namespace MyVendor\MyExtension\Backend;
+
+    use TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent;
+    use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
+
+    final class MyEventListener {
+
+        public function __invoke(
+            ModifyAutoCreateRedirectRecordBeforePersistingEvent $event
+        ): void {
+
+            // only work on plain slug replacement redirect sources.
+            if (!($event->getSource() instanceof PlainSlugReplacementRedirectSource)) {
+                return;
+            }
+
+            // Get prepared redirect record and change some values
+            $record = $event->getRedirectRecord();
+
+            // override the status code, eventually to another value than
+            // configured in the site configuration
+            $record['status_code'] = 307;
+
+            // Set value to a field extended by a custom extension, to persist
+            // additional data to the redirect record.
+            $record['custom_field_added_by_a_extension']
+                = 'page_' . $event->getSlugRedirectChangeItem()->getPageUid();
+
+            // Update changed record in event to ensure changed values are saved.
+            $event->setRedirectRecord($record);
+        }
+    }
+
+
+Impact
+======
+
+With the new :php:`ModifyAutoCreateRedirectRecordBeforePersistingEvent`, it's now
+possible to modify the auto-create redirect record before it is persisted to the database.
+Manually created redirects or updated redirects can be handled by using the well known
+:php:`\TYPO3\CMS\Core\DataHandling\DataHandler` and the available hooks.
+
+.. index:: PHP-API, ext:redirects
diff --git a/typo3/sysext/redirects/Classes/Event/AfterAutoCreateRedirectHasBeenPersistedEvent.php b/typo3/sysext/redirects/Classes/Event/AfterAutoCreateRedirectHasBeenPersistedEvent.php
new file mode 100644
index 000000000000..b51cb2980a10
--- /dev/null
+++ b/typo3/sysext/redirects/Classes/Event/AfterAutoCreateRedirectHasBeenPersistedEvent.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Redirects\Event;
+
+use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;
+use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
+
+/**
+ * This event is fired in the \TYPO3\CMS\Redirects\Service\SlugService after
+ * a redirect record has been automatically created and persisted after page
+ * slug change. It's mainly a pure notification event.
+ *
+ * It can be used to update redirects external in a load-balancer directly for
+ * example, or doing some kind of synchronization.
+ */
+final class AfterAutoCreateRedirectHasBeenPersistedEvent
+{
+    public function __construct(
+        private readonly SlugRedirectChangeItem $slugRedirectChangeItem,
+        private readonly RedirectSourceInterface $source,
+        private readonly array $redirectRecord,
+    ) {
+    }
+
+    public function getSlugRedirectChangeItem(): SlugRedirectChangeItem
+    {
+        return $this->slugRedirectChangeItem;
+    }
+
+    public function getSource(): RedirectSourceInterface
+    {
+        return $this->source;
+    }
+
+    public function getRedirectRecord(): array
+    {
+        return $this->redirectRecord;
+    }
+}
diff --git a/typo3/sysext/redirects/Classes/Event/ModifyAutoCreateRedirectRecordBeforePersistingEvent.php b/typo3/sysext/redirects/Classes/Event/ModifyAutoCreateRedirectRecordBeforePersistingEvent.php
new file mode 100644
index 000000000000..b5789da9fd88
--- /dev/null
+++ b/typo3/sysext/redirects/Classes/Event/ModifyAutoCreateRedirectRecordBeforePersistingEvent.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Redirects\Event;
+
+use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceInterface;
+use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
+
+/**
+ * This event is fired in the \TYPO3\CMS\Redirects\Service\SlugService before
+ * a redirect record is persisted for changed page slug.
+ *
+ * It can be used to modify the redirect record before persisting it. This
+ * gives extension developers the ability to apply defaults or add custom
+ * values to the record.
+ */
+final class ModifyAutoCreateRedirectRecordBeforePersistingEvent
+{
+    public function __construct(
+        private readonly SlugRedirectChangeItem $slugRedirectChangeItem,
+        private readonly RedirectSourceInterface $source,
+        private array $redirectRecord,
+    ) {
+    }
+
+    public function getSlugRedirectChangeItem(): SlugRedirectChangeItem
+    {
+        return $this->slugRedirectChangeItem;
+    }
+
+    public function getSource(): RedirectSourceInterface
+    {
+        return $this->source;
+    }
+
+    public function getRedirectRecord(): array
+    {
+        return $this->redirectRecord;
+    }
+
+    public function setRedirectRecord(array $redirectRecord): void
+    {
+        $this->redirectRecord = $redirectRecord;
+    }
+}
diff --git a/typo3/sysext/redirects/Classes/Service/SlugService.php b/typo3/sysext/redirects/Classes/Service/SlugService.php
index 4bc5a4796ee2..fcb41e4b968f 100644
--- a/typo3/sysext/redirects/Classes/Service/SlugService.php
+++ b/typo3/sysext/redirects/Classes/Service/SlugService.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Redirects\Service;
 
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
@@ -40,6 +41,8 @@ use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent;
+use TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent;
 use TYPO3\CMS\Redirects\Hooks\DataHandlerSlugUpdateHook;
 use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
 use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItemFactory;
@@ -98,6 +101,7 @@ class SlugService implements LoggerAwareInterface
         protected readonly LinkService $linkService,
         protected readonly RedirectCacheService $redirectCacheService,
         protected readonly SlugRedirectChangeItemFactory $slugRedirectChangeItemFactory,
+        protected readonly EventDispatcherInterface $eventDispatcher,
     ) {
     }
 
@@ -187,7 +191,14 @@ class SlugService implements LoggerAwareInterface
                 'disable_hitcount' => 0,
                 'creation_type' => 0,
             ];
-            // @todo Add a pre-create event here, which can be used to change the record before it is persisted.
+
+            $record = $this->eventDispatcher->dispatch(
+                new ModifyAutoCreateRedirectRecordBeforePersistingEvent(
+                    slugRedirectChangeItem: $changeItem,
+                    source: $source,
+                    redirectRecord: $record,
+                )
+            )->getRedirectRecord();
             // @todo Use dataHandler to create records
             $connection = GeneralUtility::makeInstance(ConnectionPool::class)
                 ->getConnectionForTable('sys_redirect');
@@ -195,7 +206,13 @@ class SlugService implements LoggerAwareInterface
             $id = (int)$connection->lastInsertId('sys_redirect');
             $record['uid'] = $id;
             $this->getRecordHistoryStore()->addRecord('sys_redirect', $id, $record, $this->correlationIdRedirectCreation);
-            // @todo Add a post-create event here, thus extensions can trigger stuff based on the created redirect.
+            $this->eventDispatcher->dispatch(
+                new AfterAutoCreateRedirectHasBeenPersistedEvent(
+                    slugRedirectChangeItem: $changeItem,
+                    source: $source,
+                    redirectRecord: $record,
+                )
+            );
             if (!in_array($source->getHost(), $sourceHosts)) {
                 $sourceHosts[] = $source->getHost();
             }
diff --git a/typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_AfterAutoCreateRedirectHasBeenPersistedEvent.csv b/typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_AfterAutoCreateRedirectHasBeenPersistedEvent.csv
new file mode 100644
index 000000000000..61243b3cd9a8
--- /dev/null
+++ b/typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_AfterAutoCreateRedirectHasBeenPersistedEvent.csv
@@ -0,0 +1,4 @@
+"pages",,,,,,,
+,"uid","pid","title","slug","l10n_parent","l10n_source","sys_language_uid"
+,1,0,"Root 1","/",0,0,0
+,2,1,"Dummy 1-2","/dummy-1-2",0,0,0
diff --git a/typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_ModifyAutoCreateRedirectRecordBeforePersistingEvent.csv b/typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_ModifyAutoCreateRedirectRecordBeforePersistingEvent.csv
new file mode 100644
index 000000000000..61243b3cd9a8
--- /dev/null
+++ b/typo3/sysext/redirects/Tests/Functional/Service/Fixtures/SlugServiceTest_ModifyAutoCreateRedirectRecordBeforePersistingEvent.csv
@@ -0,0 +1,4 @@
+"pages",,,,,,,
+,"uid","pid","title","slug","l10n_parent","l10n_source","sys_language_uid"
+,1,0,"Root 1","/",0,0,0
+,2,1,"Dummy 1-2","/dummy-1-2",0,0,0
diff --git a/typo3/sysext/redirects/Tests/Functional/Service/SlugServiceTest.php b/typo3/sysext/redirects/Tests/Functional/Service/SlugServiceTest.php
index 909d42e73edf..057a290cb422 100644
--- a/typo3/sysext/redirects/Tests/Functional/Service/SlugServiceTest.php
+++ b/typo3/sysext/redirects/Tests/Functional/Service/SlugServiceTest.php
@@ -17,17 +17,23 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Redirects\Tests\Functional\Service;
 
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Log\NullLogger;
+use Symfony\Component\DependencyInjection\Container;
 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\DataHandling\Model\CorrelationId;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
+use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Routing\SiteMatcher;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent;
+use TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent;
+use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
 use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItemFactory;
 use TYPO3\CMS\Redirects\Service\RedirectCacheService;
 use TYPO3\CMS\Redirects\Service\SlugService;
@@ -403,6 +409,103 @@ class SlugServiceTest extends FunctionalTestCase
         $this->assertSlugsAndRedirectsExists($slugs, $redirects);
     }
 
+    /**
+     * @test
+     */
+    public function modifyAutoCreateRedirectRecordBeforePersistingIsTriggered(): void
+    {
+        $newPageSlug = '/test-new';
+        $eventOverrideSource = '/overridden-new';
+        $this->buildBaseSiteWithLanguages();
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/SlugServiceTest_ModifyAutoCreateRedirectRecordBeforePersistingEvent.csv');
+
+        /** @var Container $container */
+        $container = $this->getContainer();
+        $container->set(
+            'modify-auto-create-redirect-record-before-persisting',
+            static function (ModifyAutoCreateRedirectRecordBeforePersistingEvent $event) use (
+                &$modifyAutoCreateRedirectRecordBeforePersisting,
+                $eventOverrideSource
+            ) {
+                $modifyAutoCreateRedirectRecordBeforePersisting = $event;
+                $event->setRedirectRecord(
+                    array_replace(
+                        $event->getRedirectRecord(),
+                        [
+                            'source_path' => $eventOverrideSource,
+                        ],
+                    )
+                );
+            }
+        );
+        $listenerProvider = $container->get(ListenerProvider::class);
+        $listenerProvider->addListener(ModifyAutoCreateRedirectRecordBeforePersistingEvent::class, 'modify-auto-create-redirect-record-before-persisting');
+        $this->createSubject();
+
+        /** @var SlugRedirectChangeItem $changeItem */
+        $changeItem = $this->get(SlugRedirectChangeItemFactory::class)->create(2);
+        $changeItem = $changeItem->withChanged(array_merge($changeItem->getOriginal(), ['slug' => $newPageSlug]));
+        $this->subject->rebuildSlugsForSlugChange(2, $changeItem, $this->correlationId);
+        $this->setPageSlug(2, $newPageSlug);
+
+        self::assertInstanceOf(ModifyAutoCreateRedirectRecordBeforePersistingEvent::class, $modifyAutoCreateRedirectRecordBeforePersisting);
+        self::assertSame($eventOverrideSource, $modifyAutoCreateRedirectRecordBeforePersisting->getRedirectRecord()['source_path']);
+
+        $this->assertSlugsAndRedirectsExists(
+            slugs: [
+                '/',
+                $newPageSlug,
+            ],
+            redirects: [
+                ['source_host' => '*', 'source_path' => $eventOverrideSource, 'target' => 't3://page?uid=2&_language=0'],
+            ],
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function afterAutoCreteRedirectHasBeenPersistedIsTriggered(): void
+    {
+        $newPageSlug = '/test-new';
+        $this->buildBaseSiteWithLanguages();
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/SlugServiceTest_AfterAutoCreateRedirectHasBeenPersistedEvent.csv');
+
+        /** @var Container $container */
+        $container = $this->getContainer();
+        $container->set(
+            'after-auto-create-redirect-has-been-persisted',
+            static function (AfterAutoCreateRedirectHasBeenPersistedEvent $event) use (
+                &$afterAutoCreateRedirectHasBeenPersisted
+            ) {
+                $afterAutoCreateRedirectHasBeenPersisted = $event;
+            }
+        );
+        $listenerProvider = $container->get(ListenerProvider::class);
+        $listenerProvider->addListener(AfterAutoCreateRedirectHasBeenPersistedEvent::class, 'after-auto-create-redirect-has-been-persisted');
+        $this->createSubject();
+
+        /** @var SlugRedirectChangeItem $changeItem */
+        $changeItem = $this->get(SlugRedirectChangeItemFactory::class)->create(2);
+        $changeItem = $changeItem->withChanged(array_merge($changeItem->getOriginal(), ['slug' => $newPageSlug]));
+        $this->subject->rebuildSlugsForSlugChange(2, $changeItem, $this->correlationId);
+        $this->setPageSlug(2, $newPageSlug);
+
+        self::assertInstanceOf(AfterAutoCreateRedirectHasBeenPersistedEvent::class, $afterAutoCreateRedirectHasBeenPersisted);
+        self::assertSame(1, $afterAutoCreateRedirectHasBeenPersisted->getRedirectRecord()['uid'] ?? null);
+
+        $this->assertSlugsAndRedirectsExists(
+            slugs: [
+                '/',
+                $newPageSlug,
+            ],
+            redirects: [
+                ['uid' => 1, 'source_host' => '*', 'source_path' => '/en/dummy-1-2', 'target' => 't3://page?uid=2&_language=0'],
+            ],
+            withRedirectUid: true,
+        );
+    }
+
     protected function buildBaseSite(): void
     {
         $configuration = [
@@ -457,17 +560,18 @@ class SlugServiceTest extends FunctionalTestCase
     {
         GeneralUtility::makeInstance(SiteMatcher::class)->refresh();
         $this->subject = new SlugService(
-            GeneralUtility::makeInstance(Context::class),
-            GeneralUtility::makeInstance(SiteFinder::class),
-            GeneralUtility::makeInstance(PageRepository::class),
-            GeneralUtility::makeInstance(LinkService::class),
-            GeneralUtility::makeInstance(RedirectCacheService::class),
-            $this->get(SlugRedirectChangeItemFactory::class),
+            context: GeneralUtility::makeInstance(Context::class),
+            siteFinder: GeneralUtility::makeInstance(SiteFinder::class),
+            pageRepository: GeneralUtility::makeInstance(PageRepository::class),
+            linkService: GeneralUtility::makeInstance(LinkService::class),
+            redirectCacheService: GeneralUtility::makeInstance(RedirectCacheService::class),
+            slugRedirectChangeItemFactory: $this->get(SlugRedirectChangeItemFactory::class),
+            eventDispatcher: $this->get(EventDispatcherInterface::class),
         );
         $this->subject->setLogger(new NullLogger());
     }
 
-    protected function assertSlugsAndRedirectsExists(array $slugs, array $redirects): void
+    protected function assertSlugsAndRedirectsExists(array $slugs, array $redirects, bool $withRedirectUid = false): void
     {
         $pageRecords = $this->getAllRecords('pages');
         self::assertCount(count($slugs), $pageRecords);
@@ -483,6 +587,14 @@ class SlugServiceTest extends FunctionalTestCase
                 'source_path' => $record['source_path'],
                 'target' => $record['target'],
             ];
+            if ($withRedirectUid) {
+                $combination = [
+                    'uid' => $record['uid'],
+                    'source_host' => $record['source_host'],
+                    'source_path' => $record['source_path'],
+                    'target' => $record['target'],
+                ];
+            }
             self::assertContains($combination, $redirects, 'wrong redirect found');
         }
     }
diff --git a/typo3/sysext/redirects/Tests/Unit/Event/AfterAutoCreateRedirectHasBeenPersistedEventTest.php b/typo3/sysext/redirects/Tests/Unit/Event/AfterAutoCreateRedirectHasBeenPersistedEventTest.php
new file mode 100644
index 000000000000..0f28a913b99b
--- /dev/null
+++ b/typo3/sysext/redirects/Tests/Unit/Event/AfterAutoCreateRedirectHasBeenPersistedEventTest.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Redirects\Tests\Unit\Event;
+
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
+use TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent;
+use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
+use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
+use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class AfterAutoCreateRedirectHasBeenPersistedEventTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function afterAutoCreateRedirectHasBeenPersistedGettersReturnsCreationValues(): void
+    {
+        $source = new PlainSlugReplacementRedirectSource(
+            host: '*',
+            path: '/some-path',
+            targetLinkParameters: []
+        );
+        $changeItem = new SlugRedirectChangeItem(
+            defaultLanguagePageId: 1,
+            pageId: 1,
+            site: $this->createMock(Site::class),
+            siteLanguage: $this->createMock(SiteLanguage::class),
+            original: ['original'],
+            sourcesCollection: new RedirectSourceCollection($source),
+            changed: ['changed'],
+        );
+        $redirectRecord = ['redirect-record'];
+
+        $event = new AfterAutoCreateRedirectHasBeenPersistedEvent(
+            slugRedirectChangeItem: $changeItem,
+            source: $source,
+            redirectRecord: $redirectRecord,
+        );
+
+        self::assertSame($source, $event->getSource());
+        self::assertSame($changeItem, $event->getSlugRedirectChangeItem());
+        self::assertSame($redirectRecord, $event->getRedirectRecord());
+    }
+}
diff --git a/typo3/sysext/redirects/Tests/Unit/Event/ModifyAutoCreateRedirectRecordBeforePersistingEventTest.php b/typo3/sysext/redirects/Tests/Unit/Event/ModifyAutoCreateRedirectRecordBeforePersistingEventTest.php
new file mode 100644
index 000000000000..154408c6a3c0
--- /dev/null
+++ b/typo3/sysext/redirects/Tests/Unit/Event/ModifyAutoCreateRedirectRecordBeforePersistingEventTest.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Redirects\Tests\Unit\Event;
+
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
+use TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent;
+use TYPO3\CMS\Redirects\RedirectUpdate\PlainSlugReplacementRedirectSource;
+use TYPO3\CMS\Redirects\RedirectUpdate\RedirectSourceCollection;
+use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class ModifyAutoCreateRedirectRecordBeforePersistingEventTest extends UnitTestCase
+{
+    public function modifyAutoCreateRedirectRecordBeforePersistingGettersReturnInstantiatingValues(): void
+    {
+        $source = new PlainSlugReplacementRedirectSource(
+            host: '*',
+            path: '/some-path',
+            targetLinkParameters: []
+        );
+        $changeItem = new SlugRedirectChangeItem(
+            defaultLanguagePageId: 1,
+            pageId: 1,
+            site: $this->createMock(Site::class),
+            siteLanguage: $this->createMock(SiteLanguage::class),
+            original: ['original'],
+            sourcesCollection: new RedirectSourceCollection($source),
+            changed: ['changed'],
+        );
+        $redirectRecord = ['redirect-record'];
+
+        $event = new ModifyAutoCreateRedirectRecordBeforePersistingEvent(
+            slugRedirectChangeItem: $changeItem,
+            source: $source,
+            redirectRecord: $redirectRecord,
+        );
+
+        self::assertSame($source, $event->getSource());
+        self::assertSame($changeItem, $event->getSlugRedirectChangeItem());
+        self::assertSame($redirectRecord, $event->getRedirectRecord());
+    }
+
+    /**
+     * @test
+     */
+    public function modifyAutoCreateRedirectRecordBeforePersistingRedirectRecordCanBeSet(): void
+    {
+        $source = new PlainSlugReplacementRedirectSource(
+            host: '*',
+            path: '/some-path',
+            targetLinkParameters: []
+        );
+        $changeItem = new SlugRedirectChangeItem(
+            defaultLanguagePageId: 1,
+            pageId: 1,
+            site: $this->createMock(Site::class),
+            siteLanguage: $this->createMock(SiteLanguage::class),
+            original: ['original'],
+            sourcesCollection: new RedirectSourceCollection($source),
+            changed: ['changed'],
+        );
+        $redirectRecord = ['redirect-record'];
+
+        $event = new ModifyAutoCreateRedirectRecordBeforePersistingEvent(
+            slugRedirectChangeItem: $changeItem,
+            source: $source,
+            redirectRecord: $redirectRecord,
+        );
+
+        self::assertSame($source, $event->getSource());
+        self::assertSame($changeItem, $event->getSlugRedirectChangeItem());
+        self::assertSame($redirectRecord, $event->getRedirectRecord());
+
+        $changedRedirectRecord = ['changed-record'];
+        $event->setRedirectRecord($changedRedirectRecord);
+
+        self::assertSame($source, $event->getSource());
+        self::assertSame($changeItem, $event->getSlugRedirectChangeItem());
+        self::assertSame($changedRedirectRecord, $event->getRedirectRecord());
+    }
+}
-- 
GitLab