From 43e3efc5a2f05ba83fbf9072500293bf0d424354 Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Tue, 19 May 2026 02:22:24 +0500 Subject: [PATCH] feat(wpmc 1.1.0): Redirect::handle reverse-slug lookup on is_404 Replace old triggering-on-page_new semantics with washanyanya-style: - template_redirect@1 + is_404() trigger - get_posts(name=$slug, post_type=page, status=[private,publish]) - match candidate.ID against page_unic[i].page_new - 301 to /// Closes Gitea issue #1. Co-Authored-By: Claude Opus 4.7 --- includes/class-redirect.php | 67 ++++--- tests/RedirectTest.php | 289 +++++++++++++++++++++++++++---- tests/ZRedirectDoingAjaxTest.php | 30 ++++ 3 files changed, 332 insertions(+), 54 deletions(-) create mode 100644 tests/ZRedirectDoingAjaxTest.php diff --git a/includes/class-redirect.php b/includes/class-redirect.php index 43b499a..b66ff38 100644 --- a/includes/class-redirect.php +++ b/includes/class-redirect.php @@ -12,60 +12,85 @@ final class Redirect public static function handle(): void { - if (\is_admin()) { + if (\is_admin() || (\defined('DOING_AJAX') && \DOING_AJAX)) { return; } - if (\defined('DOING_AJAX') && \DOING_AJAX) { + if (!\is_404()) { return; } + $main = Helpers::main_town_slug(); if ($main === null) { return; } - global $post; - if (!isset($post->ID)) { + + $slug = self::extract_last_segment($_SERVER['REQUEST_URI'] ?? '/'); + if ($slug === null) { return; } - $current_page_id = (int) $post->ID; + + $candidates = \get_posts([ + 'name' => $slug, + 'post_type' => 'page', + 'post_status' => ['private', 'publish'], + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'suppress_filters' => false, + ]); + if (empty($candidates)) { + return; + } + + $candidate_id = (int) $candidates[0]->ID; + if (!\function_exists('get_field')) { return; } - $page_unic = \get_field('page_unic', 'option'); - if (!is_array($page_unic)) { + $rows = \get_field('page_unic', 'option'); + if (!is_array($rows)) { return; } - $request_uri = $_SERVER['REQUEST_URI'] ?? '/'; - foreach ($page_unic as $row) { + + foreach ($rows as $row) { if (!is_array($row)) { continue; } - if ((int) ($row['page_new'] ?? 0) !== $current_page_id) { + if ((int) ($row['page_new'] ?? 0) !== $candidate_id) { continue; } $town_term_id = (int) ($row['town'] ?? 0); - if (!$town_term_id) { + $orig_id = (int) ($row['page_old'] ?? 0); + if (!$town_term_id || !$orig_id) { continue; } $term = \get_term_by('id', $town_term_id, 'town'); if (!($term instanceof \WP_Term) || $term->slug === '' || $term->slug === $main) { continue; } - $town_slug = $term->slug; - if (preg_match('#^/' . preg_quote($town_slug, '#') . '(/|$)#', $request_uri)) { - return; - } - $orig_id = (int) ($row['page_old'] ?? 0); - if (!$orig_id) { - return; - } - $orig_permalink = Helpers::without_url_filters(static fn () => \get_permalink($orig_id)); + $orig_permalink = Helpers::without_url_filters( + static fn () => \get_permalink($orig_id) + ); if (!is_string($orig_permalink) || $orig_permalink === '') { return; } $path_only = Helpers::remove_domain_link($orig_permalink); - $redirect_to = '/' . $town_slug . $path_only; + $redirect_to = '/' . $term->slug . $path_only; \wp_safe_redirect($redirect_to, 301); exit; } } + + private static function extract_last_segment(string $uri): ?string + { + $path = strtok($uri, '?'); + if ($path === false) { + return null; + } + $parts = array_values(array_filter( + explode('/', $path), + static fn (string $p) => $p !== '' + )); + $last = end($parts); + return $last !== false && $last !== '' ? $last : null; + } } diff --git a/tests/RedirectTest.php b/tests/RedirectTest.php index e36a47b..9ee2b01 100644 --- a/tests/RedirectTest.php +++ b/tests/RedirectTest.php @@ -18,71 +18,86 @@ final class RedirectTest extends TestCase ); } - public function test_handle_skips_when_current_town_equals_main(): void + public function test_bail_when_is_admin(): void { \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(true); \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); - \Brain\Monkey\Functions\when('is_admin')->justReturn(false); - $_SERVER['REQUEST_URI'] = '/spb/about/'; - \Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb']); - \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { - if ($name === 'main_town_slug') return 'spb'; - return false; - }); Redirect::handle(); self::assertTrue(true); } - public function test_handle_skips_when_post_not_set(): void + public function test_bail_when_not_404(): void { \Brain\Monkey\Functions\stubs(['did_action' => 1]); - \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); \Brain\Monkey\Functions\when('is_admin')->justReturn(false); - $_SERVER['REQUEST_URI'] = '/about/'; - \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); - \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { - if ($name === 'main_town_slug') return 'spb'; - return false; - }); - unset($GLOBALS['post']); + \Brain\Monkey\Functions\when('is_404')->justReturn(false); + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); Redirect::handle(); self::assertTrue(true); } - public function test_handle_skips_when_url_already_has_town_prefix(): void + public function test_bail_when_main_town_slug_null(): void { \Brain\Monkey\Functions\stubs(['did_action' => 1]); - \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); \Brain\Monkey\Functions\when('is_admin')->justReturn(false); - $_SERVER['REQUEST_URI'] = '/tyumen/about/'; - \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { - if ($name === 'main_town_slug') return 'spb'; + if ($name === 'main_town_slug') return null; return false; }); - $GLOBALS['post'] = (object) ['ID' => 84]; + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); Redirect::handle(); self::assertTrue(true); } - public function test_handle_redirects_when_post_is_page_unic_clone_without_prefix(): void + public function test_bail_when_request_uri_root(): void { \Brain\Monkey\Functions\stubs(['did_action' => 1]); \Brain\Monkey\Functions\when('is_admin')->justReturn(false); - $_SERVER['REQUEST_URI'] = '/about/'; - \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + return false; + }); + $_SERVER['REQUEST_URI'] = '/'; + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); + \Brain\Monkey\Functions\expect('get_posts')->never(); - $term = new \WP_Term(); - $term->term_id = 11; - $term->slug = 'tyumen'; - \Brain\Monkey\Functions\when('get_term_by')->justReturn($term); + Redirect::handle(); + self::assertTrue(true); + } + + public function test_bail_when_no_post_with_slug(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-tyumen/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([]); + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); + + Redirect::handle(); + + self::assertTrue(true); + } + + public function test_bail_when_post_not_in_page_unic(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { if ($name === 'main_town_slug') return 'spb'; if ($name === 'page_unic') return [ @@ -90,12 +105,94 @@ final class RedirectTest extends TestCase ]; return false; }); - $GLOBALS['post'] = (object) ['ID' => 84]; - \Brain\Monkey\Functions\when('get_permalink')->justReturn('https://site.com/about/'); + $_SERVER['REQUEST_URI'] = '/orphan-slug/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 999], + ]); + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); + + Redirect::handle(); + + self::assertTrue(true); + } + + public function test_bail_when_page_unic_empty(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + if ($name === 'page_unic') return null; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-tyumen/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 84], + ]); + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); + + Redirect::handle(); + + self::assertTrue(true); + } + + public function test_bail_when_matched_town_is_main(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + if ($name === 'page_unic') return [ + ['town' => 10, 'page_old' => 42, 'page_new' => 84], + ]; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-spb/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 84], + ]); + + $term = new \WP_Term(); + $term->term_id = 10; + $term->slug = 'spb'; + \Brain\Monkey\Functions\when('get_term_by')->justReturn($term); + + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); + + Redirect::handle(); + + self::assertTrue(true); + } + + public function test_redirect_for_private_clone(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + if ($name === 'page_unic') return [ + ['town' => 11, 'page_old' => 42, 'page_new' => 84], + ]; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-tyumen/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 84, 'post_status' => 'private'], + ]); + + $term = new \WP_Term(); + $term->term_id = 11; + $term->slug = 'tyumen'; + \Brain\Monkey\Functions\when('get_term_by')->justReturn($term); + + \Brain\Monkey\Functions\when('get_permalink')->justReturn('https://site.com/tarif/'); \Brain\Monkey\Functions\expect('wp_safe_redirect') ->once() - ->with('/tyumen/about/', 301) + ->with('/tyumen/tarif/', 301) ->andThrow(new \RuntimeException('redirect-called')); try { @@ -105,4 +202,130 @@ final class RedirectTest extends TestCase self::assertSame('redirect-called', $e->getMessage()); } } + + public function test_redirect_for_published_clone_with_own_url(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + if ($name === 'page_unic') return [ + ['town' => 11, 'page_old' => 42, 'page_new' => 84], + ]; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-tyumen/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 84, 'post_status' => 'publish'], + ]); + + $term = new \WP_Term(); + $term->term_id = 11; + $term->slug = 'tyumen'; + \Brain\Monkey\Functions\when('get_term_by')->justReturn($term); + + \Brain\Monkey\Functions\when('get_permalink')->justReturn('https://site.com/tarif/'); + + \Brain\Monkey\Functions\expect('wp_safe_redirect') + ->once() + ->with('/tyumen/tarif/', 301) + ->andThrow(new \RuntimeException('redirect-called')); + + try { + Redirect::handle(); + self::fail('Expected redirect to be issued'); + } catch (\RuntimeException $e) { + self::assertSame('redirect-called', $e->getMessage()); + } + } + + public function test_redirect_strips_domain_from_get_permalink(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + if ($name === 'page_unic') return [ + ['town' => 11, 'page_old' => 42, 'page_new' => 84], + ]; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-tyumen/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 84], + ]); + + $term = new \WP_Term(); + $term->term_id = 11; + $term->slug = 'tyumen'; + \Brain\Monkey\Functions\when('get_term_by')->justReturn($term); + + \Brain\Monkey\Functions\when('get_permalink')->justReturn('https://site.com/tarif/'); + + \Brain\Monkey\Functions\expect('wp_safe_redirect') + ->once() + ->with('/tyumen/tarif/', 301) + ->andThrow(new \RuntimeException('redirect-called')); + + try { + Redirect::handle(); + self::fail('Expected redirect to be issued'); + } catch (\RuntimeException $e) { + self::assertSame('redirect-called', $e->getMessage()); + } + } + + public function test_get_permalink_called_inside_without_url_filters(): void + { + \Brain\Monkey\Functions\stubs(['did_action' => 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\when('is_404')->justReturn(true); + \Brain\Monkey\Functions\when('get_field')->alias(function ($name) { + if ($name === 'main_town_slug') return 'spb'; + if ($name === 'page_unic') return [ + ['town' => 11, 'page_old' => 42, 'page_new' => 84], + ]; + return false; + }); + $_SERVER['REQUEST_URI'] = '/tarif-tyumen/'; + \Brain\Monkey\Functions\when('get_posts')->justReturn([ + (object) ['ID' => 84], + ]); + + $term = new \WP_Term(); + $term->term_id = 11; + $term->slug = 'tyumen'; + \Brain\Monkey\Functions\when('get_term_by')->justReturn($term); + + $depth_during_call = -1; + \Brain\Monkey\Functions\when('get_permalink')->alias( + function () use (&$depth_during_call) { + $depth_during_call = \WPMultiCity\Helpers::is_bypassed() ? 1 : 0; + return 'https://site.com/tarif/'; + } + ); + + \Brain\Monkey\Functions\expect('wp_safe_redirect') + ->once() + ->andThrow(new \RuntimeException('redirect-called')); + + try { + Redirect::handle(); + } catch (\RuntimeException $e) { + // expected + } + + self::assertSame( + 1, + $depth_during_call, + 'get_permalink must be wrapped in Helpers::without_url_filters' + ); + self::assertFalse( + \WPMultiCity\Helpers::is_bypassed(), + 'bypass depth must be restored to 0 after handle()' + ); + } + } diff --git a/tests/ZRedirectDoingAjaxTest.php b/tests/ZRedirectDoingAjaxTest.php new file mode 100644 index 0000000..bc32ad8 --- /dev/null +++ b/tests/ZRedirectDoingAjaxTest.php @@ -0,0 +1,30 @@ + 1]); + \Brain\Monkey\Functions\when('is_admin')->justReturn(false); + \Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); + + Redirect::handle(); + + self::assertTrue(true); + } +}