From d41ab548c032b97a0074673a5763869f3ce89ea8 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Sat, 14 Mar 2026 20:15:56 +0100 Subject: [PATCH 1/5] fix(MslsBlog): add is_home() for posts pages When a static page is set as WordPress "Posts page", is_home() returns true but is_front_page() returns false. The language switcher now detects this case and falls back to the target blog's page_for_posts when pages are not explicitly linked via MSLS. Closes #97 --- includes/MslsBlog.php | 8 +++++- tests/phpunit/TestMslsBlog.php | 45 ++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/includes/MslsBlog.php b/includes/MslsBlog.php index 7a002535..53fedcc7 100644 --- a/includes/MslsBlog.php +++ b/includes/MslsBlog.php @@ -134,12 +134,18 @@ public function get_url( $options ) { protected function get_permalink( OptionsInterface $options ) { $url = null; - $is_home = is_front_page(); + $is_home = is_front_page(); + $is_posts_page = ! is_front_page() && is_home(); switch_to_blog( $this->obj->userblog_id ); if ( $is_home || $options->has_value( $this->get_language() ) ) { $url = apply_filters( self::MSLS_GET_PERMALINK_HOOK, $options->get_permalink( $this->get_language() ), $this ); + } elseif ( $is_posts_page ) { + $page_for_posts = (int) get_option( 'page_for_posts' ); + if ( $page_for_posts > 0 ) { + $url = (string) get_permalink( $page_for_posts ); + } } restore_current_blog(); diff --git a/tests/phpunit/TestMslsBlog.php b/tests/phpunit/TestMslsBlog.php index 82dbc9ac..96196c38 100644 --- a/tests/phpunit/TestMslsBlog.php +++ b/tests/phpunit/TestMslsBlog.php @@ -52,7 +52,8 @@ public function test_get_frontpage(): void { $collection->shouldReceive( 'get_current_blog_id' )->andReturn( 2 ); Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); - Functions\expect( 'is_front_page' )->once()->andReturn( true ); + Functions\expect( 'is_front_page' )->atLeast()->once()->andReturn( true ); + Functions\expect( 'is_home' )->zeroOrMoreTimes()->andReturn( false ); Functions\expect( 'switch_to_blog' )->once(); Functions\expect( 'restore_current_blog' )->once(); @@ -70,7 +71,47 @@ public function test_get_url(): void { $collection->shouldReceive( 'get_current_blog_id' )->andReturn( 2 ); Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); - Functions\expect( 'is_front_page' )->once()->andReturn( false ); + Functions\expect( 'is_front_page' )->atLeast()->once()->andReturn( false ); + Functions\expect( 'is_home' )->atLeast()->once()->andReturn( false ); + Functions\expect( 'switch_to_blog' )->once(); + Functions\expect( 'restore_current_blog' )->once(); + + $this->assertEquals( $url, $this->MslsBlogFactory()->get_url( $option ) ); + } + + public function test_get_posts_page(): void { + $url = 'https://msls.co/sv/blogg/'; + + $option = \Mockery::mock( MslsOptions::class ); + $option->shouldReceive( 'has_value' )->once()->andReturn( false ); + + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_current_blog_id' )->andReturn( 2 ); + + Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); + Functions\expect( 'is_front_page' )->atLeast()->once()->andReturn( false ); + Functions\expect( 'is_home' )->atLeast()->once()->andReturn( true ); + Functions\expect( 'switch_to_blog' )->once(); + Functions\expect( 'restore_current_blog' )->once(); + Functions\expect( 'get_option' )->atLeast()->once()->andReturn( 42 ); + Functions\expect( 'get_permalink' )->once()->with( 42 )->andReturn( $url ); + + $this->assertEquals( $url, $this->MslsBlogFactory()->get_url( $option ) ); + } + + public function test_get_posts_page_with_translation(): void { + $url = 'https://msls.co/sv/blogg/'; + + $option = \Mockery::mock( MslsOptions::class ); + $option->shouldReceive( 'get_permalink' )->once()->andReturn( $url ); + $option->shouldReceive( 'has_value' )->once()->andReturn( true ); + + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_current_blog_id' )->andReturn( 2 ); + + Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); + Functions\expect( 'is_front_page' )->atLeast()->once()->andReturn( false ); + Functions\expect( 'is_home' )->atLeast()->once()->andReturn( true ); Functions\expect( 'switch_to_blog' )->once(); Functions\expect( 'restore_current_blog' )->once(); From 63a6b98eb16479bcc08d004a498cd33d4cc4a980 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Sat, 14 Mar 2026 20:16:06 +0100 Subject: [PATCH 2/5] fix(MslsOptions): prevent blog slug corruption The greedy str_replace in check_for_blog_slug() matched /blog as substring of /blogg/, producing truncated URLs like /sv/g/ instead of /sv/blogg/. Replace with boundary-aware prefix stripping that only removes the slug when followed by / or EOL. --- includes/MslsOptions.php | 7 ++++++- tests/phpunit/TestMslsOptions.php | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/includes/MslsOptions.php b/includes/MslsOptions.php index e17339e1..de00bf88 100644 --- a/includes/MslsOptions.php +++ b/includes/MslsOptions.php @@ -416,7 +416,12 @@ public static function check_for_blog_slug( $url, $options ) { if ( $permalink_structure ) { list( $needle, ) = explode( '/%', $permalink_structure, 2 ); - $url = str_replace( $needle, '', $url ); + if ( '' !== $needle && str_starts_with( $url, $needle ) ) { + $rest = substr( $url, strlen( $needle ) ); + if ( '' === $rest || '/' === $rest[0] ) { + $url = '' === $rest ? '/' : $rest; + } + } if ( is_main_site() && $options->with_front ) { $url = "{$needle}{$url}"; } diff --git a/tests/phpunit/TestMslsOptions.php b/tests/phpunit/TestMslsOptions.php index d7d24269..777ab8e0 100644 --- a/tests/phpunit/TestMslsOptions.php +++ b/tests/phpunit/TestMslsOptions.php @@ -204,6 +204,8 @@ public static function provide_data_for_slug_check(): array { array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/blog/%year%/%monthnum%/%postname%/', true ), array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/%postname%/', true ), array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/blog/%postname%/', true ), + array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', false ), + array( 'https://msls.co/blog/', 'https://msls.co/', true, true, true, '/blog/%postname%/', false ), ); } From bd56cb472aa535dee48e5ecd9a2cacc6a85720a3 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Sat, 14 Mar 2026 20:42:35 +0100 Subject: [PATCH 3/5] refactor(MslsBlog): rename $is_home to $is_front_page Rename misleading variable and apply permalink hook to the posts-page fallback for consistent filtering. --- includes/MslsBlog.php | 8 ++++---- tests/phpunit/TestMslsBlog.php | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/includes/MslsBlog.php b/includes/MslsBlog.php index 53fedcc7..fd1427ce 100644 --- a/includes/MslsBlog.php +++ b/includes/MslsBlog.php @@ -134,17 +134,17 @@ public function get_url( $options ) { protected function get_permalink( OptionsInterface $options ) { $url = null; - $is_home = is_front_page(); - $is_posts_page = ! is_front_page() && is_home(); + $is_front_page = is_front_page(); + $is_posts_page = ! $is_front_page && is_home(); switch_to_blog( $this->obj->userblog_id ); - if ( $is_home || $options->has_value( $this->get_language() ) ) { + if ( $is_front_page || $options->has_value( $this->get_language() ) ) { $url = apply_filters( self::MSLS_GET_PERMALINK_HOOK, $options->get_permalink( $this->get_language() ), $this ); } elseif ( $is_posts_page ) { $page_for_posts = (int) get_option( 'page_for_posts' ); if ( $page_for_posts > 0 ) { - $url = (string) get_permalink( $page_for_posts ); + $url = apply_filters( self::MSLS_GET_PERMALINK_HOOK, (string) get_permalink( $page_for_posts ), $this ); } } diff --git a/tests/phpunit/TestMslsBlog.php b/tests/phpunit/TestMslsBlog.php index 96196c38..ea366f94 100644 --- a/tests/phpunit/TestMslsBlog.php +++ b/tests/phpunit/TestMslsBlog.php @@ -95,6 +95,7 @@ public function test_get_posts_page(): void { Functions\expect( 'restore_current_blog' )->once(); Functions\expect( 'get_option' )->atLeast()->once()->andReturn( 42 ); Functions\expect( 'get_permalink' )->once()->with( 42 )->andReturn( $url ); + Functions\expect( 'apply_filters' )->once()->andReturn( $url ); $this->assertEquals( $url, $this->MslsBlogFactory()->get_url( $option ) ); } From ab31769eb2ca4fb7e5d4f7a33c9b2db112cd4704 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Sat, 14 Mar 2026 20:42:40 +0100 Subject: [PATCH 4/5] fix(MslsOptions): only re-add slug when stripped The is_main_site re-addition of the blog slug was unconditional, corrupting URLs where the slug was not stripped (e.g. /blogg/ becoming /blog/blogg/). --- includes/MslsOptions.php | 6 ++++-- tests/phpunit/TestMslsOptions.php | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/MslsOptions.php b/includes/MslsOptions.php index de00bf88..ba0727b5 100644 --- a/includes/MslsOptions.php +++ b/includes/MslsOptions.php @@ -416,13 +416,15 @@ public static function check_for_blog_slug( $url, $options ) { if ( $permalink_structure ) { list( $needle, ) = explode( '/%', $permalink_structure, 2 ); + $stripped = false; if ( '' !== $needle && str_starts_with( $url, $needle ) ) { $rest = substr( $url, strlen( $needle ) ); if ( '' === $rest || '/' === $rest[0] ) { - $url = '' === $rest ? '/' : $rest; + $url = '' === $rest ? '/' : $rest; + $stripped = true; } } - if ( is_main_site() && $options->with_front ) { + if ( is_main_site() && $options->with_front && $stripped ) { $url = "{$needle}{$url}"; } } diff --git a/tests/phpunit/TestMslsOptions.php b/tests/phpunit/TestMslsOptions.php index 777ab8e0..1b62491a 100644 --- a/tests/phpunit/TestMslsOptions.php +++ b/tests/phpunit/TestMslsOptions.php @@ -205,6 +205,7 @@ public static function provide_data_for_slug_check(): array { array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/%postname%/', true ), array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/blog/%postname%/', true ), array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', false ), + array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', true ), array( 'https://msls.co/blog/', 'https://msls.co/', true, true, true, '/blog/%postname%/', false ), ); } From f83a26c378ac96fede71dc14770c870a6218dfff Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Sat, 14 Mar 2026 20:55:34 +0100 Subject: [PATCH 5/5] test(MslsBlog): drop explicit apply_filters mock Brain\Monkey handles apply_filters by default. The explicit mock was inconsistent with other tests and unnecessarily fragile. --- tests/phpunit/TestMslsBlog.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/TestMslsBlog.php b/tests/phpunit/TestMslsBlog.php index ea366f94..96196c38 100644 --- a/tests/phpunit/TestMslsBlog.php +++ b/tests/phpunit/TestMslsBlog.php @@ -95,7 +95,6 @@ public function test_get_posts_page(): void { Functions\expect( 'restore_current_blog' )->once(); Functions\expect( 'get_option' )->atLeast()->once()->andReturn( 42 ); Functions\expect( 'get_permalink' )->once()->with( 42 )->andReturn( $url ); - Functions\expect( 'apply_filters' )->once()->andReturn( $url ); $this->assertEquals( $url, $this->MslsBlogFactory()->get_url( $option ) ); }