diff --git a/boot.php b/boot.php
index 566ded5..ed8660b 100644
--- a/boot.php
+++ b/boot.php
@@ -95,6 +95,26 @@
if (rex::isBackend() && rex::getUser() !== null) {
rex_view::addCssFile($addon->getAssetsUrl('styles.css'));
+
+ // Extend YRewrite forward list to show redirect source
+ if (\rex_addon::get('yrewrite')->isAvailable()) {
+ rex_extension::register('YREWRITE_FORWARD_LIST', function (rex_extension_point $ep) {
+ /** @var rex_list $list */
+ $list = $ep->getSubject();
+
+ // Add column to show if redirect was created by URL addon
+ $list->addColumn('url_addon_source', '', count($list->getColumnNames()));
+ $list->setColumnLabel('url_addon_source', rex_i18n::msg('url_generator_redirect_source'));
+ $list->setColumnFormat('url_addon_source', 'custom', function ($params) {
+ $list = $params['list'];
+ $isUrlAddon = $list->getValue('is_url_addon');
+ if ($isUrlAddon === 1) {
+ return '' . rex_i18n::msg('url_generator_redirect_from_url_addon') . '';
+ }
+ return '' . rex_i18n::msg('url_generator_redirect_manual') . '';
+ });
+ });
+ }
}
if (null !== Url::getRewriter() && Url::getRewriter()->getSeoTagsExtensionPoint() !== '') {
diff --git a/install.php b/install.php
index 3751f0f..0ab3082 100644
--- a/install.php
+++ b/install.php
@@ -85,3 +85,12 @@
$updateSql->update();
}
}
+
+// Add is_url_addon column to yrewrite_redirect table if it doesn't exist
+if (\rex_addon::get('yrewrite')->isAvailable()) {
+ \rex_sql_table::get(
+ \rex::getTable('yrewrite_redirect')
+ )
+ ->ensureColumn(new \rex_sql_column('is_url_addon', 'TINYINT(1)', false, '0'))
+ ->ensure();
+}
diff --git a/lang/de_de.lang b/lang/de_de.lang
index 476ac47..d4a676c 100644
--- a/lang/de_de.lang
+++ b/lang/de_de.lang
@@ -135,3 +135,8 @@ url.profile.not_set = keine Auswahl
// Neue Update-Seite
url_generator_update = Neue Version verfügbar
+
+# Redirect management
+url_generator_redirect_source = Quelle
+url_generator_redirect_from_url_addon = URL-AddOn
+url_generator_redirect_manual = Manuell
diff --git a/lang/en_gb.lang b/lang/en_gb.lang
index d76ec96..f73aeff 100644
--- a/lang/en_gb.lang
+++ b/lang/en_gb.lang
@@ -100,3 +100,8 @@ url_yform_value_description = Adds a link to the frontend generated by the URL a
// Löschen
url_generate_notice_url_param_key = When using rex_getUrl you no longer have to add the article Id from the chosen article above. So instead of
rex_getUrl(4, '', ['id' => 5]) you can use rex_getUrl('', '', ['news-id' => 5])
+
+# Redirect management
+url_generator_redirect_source = Source
+url_generator_redirect_from_url_addon = URL Addon
+url_generator_redirect_manual = Manual
diff --git a/lib/Url/ExtensionPointManager.php b/lib/Url/ExtensionPointManager.php
index c6e7f58..eaa41b6 100644
--- a/lib/Url/ExtensionPointManager.php
+++ b/lib/Url/ExtensionPointManager.php
@@ -89,6 +89,11 @@ public function getStructureClangId(): int|string|null
return $this->structureClangId;
}
+ public function isDatasetEditMode(): bool
+ {
+ return $this->dataEditMode ?? false;
+ }
+
protected function normalize(): void
{
switch ($this->extensionPoint->getName()) {
diff --git a/lib/Url/Generator.php b/lib/Url/Generator.php
index 001a128..47f10f0 100644
--- a/lib/Url/Generator.php
+++ b/lib/Url/Generator.php
@@ -49,8 +49,20 @@ public function execute(): void
$profiles = Profile::getByTableName($this->manager->getDatasetTableName());
if (count($profiles) > 0) {
foreach ($profiles as $profile) {
+ // Get old URLs before deletion to create redirects (only in edit mode)
+ $oldUrls = [];
+ if ($this->manager->isDatasetEditMode()) {
+ $oldUrls = UrlManagerSql::getOriginUrls($profile->getId(), $this->manager->getDatasetPrimaryId());
+ }
+
$profile->deleteUrlsByDatasetId($this->manager->getDatasetPrimaryId());
$profile->buildUrlsByDatasetId($this->manager->getDatasetPrimaryId());
+
+ // Create redirects from old to new URLs (only if we had old URLs)
+ if (!empty($oldUrls)) {
+ $newUrls = UrlManagerSql::getOriginUrls($profile->getId(), $this->manager->getDatasetPrimaryId());
+ self::createRedirectsForUrlChanges($oldUrls, $newUrls);
+ }
}
}
break;
@@ -71,4 +83,81 @@ public static function boot(): void
}
}
}
+
+ /**
+ * Creates redirects when URLs change
+ *
+ * @param array $oldUrls Old URL entries before change
+ * @param array $newUrls New URL entries after change
+ */
+ private static function createRedirectsForUrlChanges(array $oldUrls, array $newUrls): void
+ {
+ if (empty($oldUrls) || empty($newUrls)) {
+ return;
+ }
+
+ // Group by clang_id to match old and new URLs properly
+ $oldUrlsByClang = [];
+ foreach ($oldUrls as $oldUrl) {
+ $clangId = $oldUrl['clang_id'];
+ if (!isset($oldUrlsByClang[$clangId])) {
+ $oldUrlsByClang[$clangId] = [];
+ }
+ $oldUrlsByClang[$clangId][] = $oldUrl;
+ }
+
+ $newUrlsByClang = [];
+ foreach ($newUrls as $newUrl) {
+ $clangId = $newUrl['clang_id'];
+ if (!isset($newUrlsByClang[$clangId])) {
+ $newUrlsByClang[$clangId] = [];
+ }
+ $newUrlsByClang[$clangId][] = $newUrl;
+ }
+
+ // Create redirects for each language
+ foreach ($oldUrlsByClang as $clangId => $oldClangUrls) {
+ if (!isset($newUrlsByClang[$clangId])) {
+ continue;
+ }
+
+ $newClangUrls = $newUrlsByClang[$clangId];
+
+ // Match origin URLs (not user_path, not structure)
+ $oldOriginUrl = null;
+ $newOriginUrl = null;
+
+ foreach ($oldClangUrls as $url) {
+ if ($url['is_user_path'] === 0 && $url['is_structure'] === 0) {
+ $oldOriginUrl = $url['url'];
+ break;
+ }
+ }
+
+ foreach ($newClangUrls as $url) {
+ if ($url['is_user_path'] === 0 && $url['is_structure'] === 0) {
+ $newOriginUrl = $url['url'];
+ break;
+ }
+ }
+
+ if ($oldOriginUrl && $newOriginUrl && $oldOriginUrl !== $newOriginUrl) {
+ // Get domain ID for the article
+ $articleId = null;
+ if (!empty($newClangUrls)) {
+ $articleId = $newClangUrls[0]['article_id'] ?? null;
+ }
+ $domainId = 1; // default
+
+ if ($articleId && \rex_addon::get('yrewrite')->isAvailable()) {
+ $domain = \rex_yrewrite::getDomainByArticleId($articleId, $clangId);
+ if ($domain) {
+ $domainId = $domain->getId();
+ }
+ }
+
+ RedirectManager::createRedirect($oldOriginUrl, $newOriginUrl, $domainId);
+ }
+ }
+ }
}
diff --git a/lib/Url/RedirectManager.php b/lib/Url/RedirectManager.php
new file mode 100644
index 0000000..8677f0f
--- /dev/null
+++ b/lib/Url/RedirectManager.php
@@ -0,0 +1,141 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Url;
+
+class RedirectManager
+{
+ /**
+ * Creates a 301 redirect from old URL to new URL in yrewrite_redirect table
+ *
+ * @param string $sourceUrl The old URL to redirect from
+ * @param string $targetUrl The new URL to redirect to
+ * @param int $domainId The yrewrite domain ID (required, no default)
+ * @return bool True if redirect was created successfully
+ */
+ public static function createRedirect(string $sourceUrl, string $targetUrl, int $domainId): bool
+ {
+ if (!\rex_addon::get('yrewrite')->isAvailable()) {
+ return false;
+ }
+
+ // Validate URLs are non-empty
+ if (empty($sourceUrl) || empty($targetUrl)) {
+ return false;
+ }
+
+ // Don't create redirect if source and target are the same
+ if ($sourceUrl === $targetUrl) {
+ return false;
+ }
+
+ // Remove any existing redirect that would create a loop
+ // If the new target URL was previously a source URL, delete it
+ self::deleteRedirectBySource($targetUrl);
+
+ // Check if redirect already exists
+ $sql = \rex_sql::factory();
+ $existing = $sql->getArray(
+ 'SELECT id FROM ' . \rex::getTable('yrewrite_redirect') .
+ ' WHERE url_source = ? AND domain_id = ?',
+ [$sourceUrl, $domainId]
+ );
+
+ if (count($existing) > 0) {
+ // Update existing redirect
+ $sql->setTable(\rex::getTable('yrewrite_redirect'));
+ $sql->setWhere('id = ?', [$existing[0]['id']]);
+ $sql->setValue('url_target', $targetUrl);
+ $sql->setValue('status', 301);
+ $sql->setValue('is_url_addon', 1);
+ try {
+ $sql->update();
+ self::clearYrewriteCache();
+ return true;
+ } catch (\rex_sql_exception $e) {
+ \rex_logger::logException($e);
+ return false;
+ }
+ }
+
+ // Create new redirect
+ $sql = \rex_sql::factory();
+ $sql->setTable(\rex::getTable('yrewrite_redirect'));
+ $sql->setValue('domain_id', $domainId);
+ $sql->setValue('url_source', $sourceUrl);
+ $sql->setValue('url_target', $targetUrl);
+ $sql->setValue('status', 301);
+ $sql->setValue('type', 'url');
+ $sql->setValue('is_url_addon', 1);
+
+ try {
+ $sql->insert();
+ self::clearYrewriteCache();
+ return true;
+ } catch (\rex_sql_exception $e) {
+ \rex_logger::logException($e);
+ return false;
+ }
+ }
+
+ /**
+ * Deletes a redirect by its source URL to prevent loops
+ *
+ * @param string $sourceUrl The source URL of the redirect to delete
+ * @return bool True if redirect was deleted or didn't exist
+ */
+ public static function deleteRedirectBySource(string $sourceUrl): bool
+ {
+ if (!\rex_addon::get('yrewrite')->isAvailable()) {
+ return false;
+ }
+
+ $sql = \rex_sql::factory();
+ $sql->setTable(\rex::getTable('yrewrite_redirect'));
+ $sql->setWhere('url_source = ? AND is_url_addon = 1', [$sourceUrl]);
+
+ try {
+ $sql->delete();
+ self::clearYrewriteCache();
+ return true;
+ } catch (\rex_sql_exception $e) {
+ \rex_logger::logException($e);
+ return false;
+ }
+ }
+
+ /**
+ * Clears the YRewrite redirect cache
+ */
+ private static function clearYrewriteCache(): void
+ {
+ if (class_exists('\rex_yrewrite_forward')) {
+ \rex_yrewrite_forward::clearCache();
+ }
+ }
+
+ /**
+ * Gets all redirects created by the URL addon
+ *
+ * @return array Array of redirects
+ */
+ public static function getUrlAddonRedirects(): array
+ {
+ if (!\rex_addon::get('yrewrite')->isAvailable()) {
+ return [];
+ }
+
+ $sql = \rex_sql::factory();
+ return $sql->getArray(
+ 'SELECT * FROM ' . \rex::getTable('yrewrite_redirect') . ' WHERE is_url_addon = 1'
+ );
+ }
+}
diff --git a/lib/Url/UrlManagerSql.php b/lib/Url/UrlManagerSql.php
index 0fcf35c..d2afe97 100644
--- a/lib/Url/UrlManagerSql.php
+++ b/lib/Url/UrlManagerSql.php
@@ -281,6 +281,22 @@ public static function getOriginAndExpanded(Profile $profile, int $datasetId, in
return $sql->sql->getArray('SELECT * FROM '.\rex::getTable(self::TABLE_NAME).' WHERE `profile_id` = ? AND `data_id` = ? AND `clang_id` = ?', [$profile->getId(), $datasetId, $clangId]);
}
+ /**
+ * Get all URLs for a specific profile and dataset (across all languages)
+ *
+ * @param int $profileId
+ * @param int $datasetId
+ *
+ * @throws \rex_sql_exception
+ *
+ * @return array
+ */
+ public static function getOriginUrls(int $profileId, int $datasetId): array
+ {
+ $sql = self::factory();
+ return $sql->sql->getArray('SELECT * FROM '.\rex::getTable(self::TABLE_NAME).' WHERE `profile_id` = ? AND `data_id` = ?', [$profileId, $datasetId]);
+ }
+
/**
* @param Url $url
*