From 6149284e21712c60780e2dd4ecd0a68df20d7844 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:43:45 +0000 Subject: [PATCH 1/6] Initial plan From 9dd1e89aa6ab328aeca88201485fdc46dfaa2703 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:49:43 +0000 Subject: [PATCH 2/6] Add automatic redirect creation for URL changes Co-authored-by: AWqxKAWERbXo <3855487+AWqxKAWERbXo@users.noreply.github.com> --- boot.php | 20 ++++++ install.php | 9 +++ lang/de_de.lang | 5 ++ lang/en_gb.lang | 5 ++ lib/Url/Generator.php | 83 ++++++++++++++++++++++ lib/Url/RedirectManager.php | 133 ++++++++++++++++++++++++++++++++++++ lib/Url/UrlManagerSql.php | 16 +++++ 7 files changed, 271 insertions(+) create mode 100644 lib/Url/RedirectManager.php diff --git a/boot.php b/boot.php index 566ded5..2a77daa 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/Generator.php b/lib/Url/Generator.php index 001a128..9d31f23 100644 --- a/lib/Url/Generator.php +++ b/lib/Url/Generator.php @@ -49,8 +49,17 @@ 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 + $oldUrls = UrlManagerSql::getOriginUrls($profile->getId(), $this->manager->getDatasetPrimaryId()); + $profile->deleteUrlsByDatasetId($this->manager->getDatasetPrimaryId()); $profile->buildUrlsByDatasetId($this->manager->getDatasetPrimaryId()); + + // Get new URLs after building + $newUrls = UrlManagerSql::getOriginUrls($profile->getId(), $this->manager->getDatasetPrimaryId()); + + // Create redirects from old to new URLs + self::createRedirectsForUrlChanges($oldUrls, $newUrls); } } break; @@ -71,4 +80,78 @@ 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 = $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..63258d6 --- /dev/null +++ b/lib/Url/RedirectManager.php @@ -0,0 +1,133 @@ + + * + * 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 + * @return bool True if redirect was created successfully + */ + public static function createRedirect(string $sourceUrl, string $targetUrl, int $domainId = 1): bool + { + if (!\rex_addon::get('yrewrite')->isAvailable()) { + 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) { + 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) { + 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) { + 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 * From 2a9a51d9269095300d1b4776d1f7c499babe75ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:51:34 +0000 Subject: [PATCH 3/6] Improve redirect creation to only work in edit mode Co-authored-by: AWqxKAWERbXo <3855487+AWqxKAWERbXo@users.noreply.github.com> --- lib/Url/ExtensionPointManager.php | 5 +++++ lib/Url/Generator.php | 17 ++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) 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 9d31f23..91fbc8e 100644 --- a/lib/Url/Generator.php +++ b/lib/Url/Generator.php @@ -49,17 +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 - $oldUrls = UrlManagerSql::getOriginUrls($profile->getId(), $this->manager->getDatasetPrimaryId()); + // 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()); - // Get new URLs after building - $newUrls = UrlManagerSql::getOriginUrls($profile->getId(), $this->manager->getDatasetPrimaryId()); - - // Create redirects from old to new URLs - self::createRedirectsForUrlChanges($oldUrls, $newUrls); + // 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; From d052a533e1a11e798bdaa12e7e726401d6d07ade Mon Sep 17 00:00:00 2001 From: Alex <3855487+AWqxKAWERbXo@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:16:41 +0100 Subject: [PATCH 4/6] Update lib/Url/RedirectManager.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Alex <3855487+AWqxKAWERbXo@users.noreply.github.com> --- lib/Url/RedirectManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Url/RedirectManager.php b/lib/Url/RedirectManager.php index 63258d6..f7146e0 100644 --- a/lib/Url/RedirectManager.php +++ b/lib/Url/RedirectManager.php @@ -100,6 +100,7 @@ public static function deleteRedirectBySource(string $sourceUrl): bool self::clearYrewriteCache(); return true; } catch (\rex_sql_exception $e) { + \rex_logger::logException($e); return false; } } From 58d5aa2097142309db25d0902abdb81f15b9515f Mon Sep 17 00:00:00 2001 From: Alex <3855487+AWqxKAWERbXo@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:17:13 +0100 Subject: [PATCH 5/6] Update lib/Url/RedirectManager.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Alex <3855487+AWqxKAWERbXo@users.noreply.github.com> --- lib/Url/RedirectManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Url/RedirectManager.php b/lib/Url/RedirectManager.php index f7146e0..5361f7a 100644 --- a/lib/Url/RedirectManager.php +++ b/lib/Url/RedirectManager.php @@ -21,7 +21,7 @@ class RedirectManager * @param int $domainId The yrewrite domain ID * @return bool True if redirect was created successfully */ - public static function createRedirect(string $sourceUrl, string $targetUrl, int $domainId = 1): bool + public static function createRedirect(string $sourceUrl, string $targetUrl, int $domainId): bool { if (!\rex_addon::get('yrewrite')->isAvailable()) { return false; From bbbe9a6f39c907bd62d1e397b4d5a5a55bd718ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 22:20:10 +0000 Subject: [PATCH 6/6] Address code review feedback: strict comparisons, error logging, input validation Co-authored-by: AWqxKAWERbXo <3855487+AWqxKAWERbXo@users.noreply.github.com> --- boot.php | 2 +- lib/Url/Generator.php | 9 ++++++--- lib/Url/RedirectManager.php | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/boot.php b/boot.php index 2a77daa..ed8660b 100644 --- a/boot.php +++ b/boot.php @@ -108,7 +108,7 @@ $list->setColumnFormat('url_addon_source', 'custom', function ($params) { $list = $params['list']; $isUrlAddon = $list->getValue('is_url_addon'); - if ($isUrlAddon == 1) { + if ($isUrlAddon === 1) { return '' . rex_i18n::msg('url_generator_redirect_from_url_addon') . ''; } return '' . rex_i18n::msg('url_generator_redirect_manual') . ''; diff --git a/lib/Url/Generator.php b/lib/Url/Generator.php index 91fbc8e..47f10f0 100644 --- a/lib/Url/Generator.php +++ b/lib/Url/Generator.php @@ -128,14 +128,14 @@ private static function createRedirectsForUrlChanges(array $oldUrls, array $newU $newOriginUrl = null; foreach ($oldClangUrls as $url) { - if ($url['is_user_path'] == 0 && $url['is_structure'] == 0) { + 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) { + if ($url['is_user_path'] === 0 && $url['is_structure'] === 0) { $newOriginUrl = $url['url']; break; } @@ -143,7 +143,10 @@ private static function createRedirectsForUrlChanges(array $oldUrls, array $newU if ($oldOriginUrl && $newOriginUrl && $oldOriginUrl !== $newOriginUrl) { // Get domain ID for the article - $articleId = $newClangUrls[0]['article_id'] ?? null; + $articleId = null; + if (!empty($newClangUrls)) { + $articleId = $newClangUrls[0]['article_id'] ?? null; + } $domainId = 1; // default if ($articleId && \rex_addon::get('yrewrite')->isAvailable()) { diff --git a/lib/Url/RedirectManager.php b/lib/Url/RedirectManager.php index 5361f7a..8677f0f 100644 --- a/lib/Url/RedirectManager.php +++ b/lib/Url/RedirectManager.php @@ -18,7 +18,7 @@ class RedirectManager * * @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 + * @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 @@ -27,6 +27,11 @@ public static function createRedirect(string $sourceUrl, string $targetUrl, int 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; @@ -56,6 +61,7 @@ public static function createRedirect(string $sourceUrl, string $targetUrl, int self::clearYrewriteCache(); return true; } catch (\rex_sql_exception $e) { + \rex_logger::logException($e); return false; } } @@ -75,6 +81,7 @@ public static function createRedirect(string $sourceUrl, string $targetUrl, int self::clearYrewriteCache(); return true; } catch (\rex_sql_exception $e) { + \rex_logger::logException($e); return false; } }