Skip to content

Commit f53ccdb

Browse files
authored
Limit term redirect to supported taxonomies (#2734)
1 parent b681d64 commit f53ccdb

File tree

3 files changed

+288
-6
lines changed

3 files changed

+288
-6
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Fixed unwanted 301 redirects on search and posts pages when using Polylang or similar plugins.

includes/class-router.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,19 @@ public static function template_redirect() {
273273
return;
274274
}
275275

276+
/**
277+
* Filters the taxonomies supported for term redirects.
278+
*
279+
* @since unreleased
280+
*
281+
* @param array $supported_taxonomies Array of taxonomy names. Default array( 'category', 'post_tag' ).
282+
*/
283+
$supported_taxonomies = \apply_filters( 'activitypub_supported_taxonomies', array( 'category', 'post_tag' ) );
284+
285+
if ( ! in_array( $term->taxonomy, $supported_taxonomies, true ) ) {
286+
return;
287+
}
288+
276289
// Don't redirect for ActivityPub requests.
277290
if ( is_activitypub_request() ) {
278291
return;

tests/phpunit/tests/includes/class-test-router.php

Lines changed: 271 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ public function setUp(): void {
4747
Router::init();
4848
}
4949

50+
/**
51+
* Tear down test environment.
52+
*/
53+
public function tear_down(): void {
54+
// Clean up common state that may be left by tests.
55+
unset( $_SERVER['HTTP_ACCEPT'] );
56+
\set_query_var( 'preview', null );
57+
\set_query_var( 'term_id', null );
58+
Query::get_instance()->__destruct();
59+
60+
parent::tear_down();
61+
}
62+
5063
/**
5164
* Test that ActivityPub requests for custom post types return 200.
5265
*
@@ -220,19 +233,271 @@ public function test_preview_template_filter() {
220233
$_SERVER['HTTP_ACCEPT'] = 'application/activity+json';
221234
\set_query_var( 'preview', true );
222235

236+
// Save callback to variable for proper removal.
237+
$preview_template_callback = function () {
238+
return '/custom/template.php';
239+
};
240+
223241
// Add filter before testing.
224-
\add_filter(
225-
'activitypub_preview_template',
226-
function () {
227-
return '/custom/template.php';
228-
}
229-
);
242+
\add_filter( 'activitypub_preview_template', $preview_template_callback );
230243

231244
// Test that the filter is applied.
232245
$template = Router::render_activitypub_template( 'original.php' );
233246
$this->assertEquals( '/custom/template.php', $template, 'Custom preview template should be used when filter is applied.' );
234247

248+
// Clean up.
249+
\remove_filter( 'activitypub_preview_template', $preview_template_callback );
250+
}
251+
252+
/**
253+
* Test that the activitypub_supported_taxonomies filter has correct defaults.
254+
*
255+
* @covers ::template_redirect
256+
*/
257+
public function test_supported_taxonomies_filter_defaults() {
258+
$supported = \apply_filters( 'activitypub_supported_taxonomies', array( 'category', 'post_tag' ) );
259+
260+
$this->assertContains( 'category', $supported, 'Category should be a supported taxonomy by default.' );
261+
$this->assertContains( 'post_tag', $supported, 'Post tag should be a supported taxonomy by default.' );
262+
$this->assertCount( 2, $supported, 'Should have exactly 2 default supported taxonomies.' );
263+
}
264+
265+
/**
266+
* Test that the activitypub_supported_taxonomies filter can be modified.
267+
*
268+
* @covers ::template_redirect
269+
*/
270+
public function test_supported_taxonomies_filter_can_be_modified() {
271+
\add_filter(
272+
'activitypub_supported_taxonomies',
273+
function ( $taxonomies ) {
274+
$taxonomies[] = 'custom_taxonomy';
275+
return $taxonomies;
276+
}
277+
);
278+
279+
$supported = \apply_filters( 'activitypub_supported_taxonomies', array( 'category', 'post_tag' ) );
280+
281+
$this->assertContains( 'custom_taxonomy', $supported, 'Custom taxonomy should be added via filter.' );
282+
$this->assertCount( 3, $supported, 'Should have 3 taxonomies after adding custom one.' );
283+
284+
// Clean up.
285+
\remove_all_filters( 'activitypub_supported_taxonomies' );
286+
}
287+
288+
/**
289+
* Test that unsupported taxonomy terms don't trigger redirects.
290+
*
291+
* This test verifies the fix for #2730 (Polylang conflict) and #2725 (posts page redirect).
292+
* When a term_id belongs to an unsupported taxonomy, the router should return early
293+
* without redirecting. The unsupported taxonomy check happens before the ActivityPub
294+
* request check, so no HTTP_ACCEPT header is needed.
295+
*
296+
* @covers ::template_redirect
297+
*/
298+
public function test_unsupported_taxonomy_does_not_redirect() {
299+
// Register a custom taxonomy (simulating Polylang's language taxonomy).
300+
\register_taxonomy(
301+
'language',
302+
'post',
303+
array(
304+
'public' => true,
305+
'label' => 'Language',
306+
)
307+
);
308+
309+
// Create a term in the custom taxonomy.
310+
$term = \wp_insert_term( 'English', 'language' );
311+
$this->assertNotWPError( $term, 'Term creation should succeed.' );
312+
313+
$term_id = $term['term_id'];
314+
315+
// Set the term_id query var (simulating what might happen with Polylang).
316+
\set_query_var( 'term_id', $term_id );
317+
318+
global $wp_query;
319+
320+
// Call template_redirect - it should return early for unsupported taxonomy.
321+
// Note: No HTTP_ACCEPT header needed because the taxonomy check happens first.
322+
Router::template_redirect();
323+
324+
// The query should not be set to 404 for valid but unsupported taxonomy terms.
325+
$this->assertFalse( $wp_query->is_404(), 'Should not set 404 for valid unsupported taxonomy terms.' );
326+
327+
// Clean up.
328+
\set_query_var( 'term_id', null );
329+
\wp_delete_term( $term_id, 'language' );
330+
\unregister_taxonomy( 'language' );
331+
}
332+
333+
/**
334+
* Test that supported taxonomy terms are handled correctly for ActivityPub requests.
335+
*
336+
* @covers ::template_redirect
337+
*/
338+
public function test_supported_taxonomy_activitypub_request_no_redirect() {
339+
// Create a category term.
340+
$term = \wp_insert_term( 'Test Category', 'category' );
341+
$this->assertNotWPError( $term, 'Term creation should succeed.' );
342+
343+
$term_id = $term['term_id'];
344+
345+
// Set the term_id query var.
346+
\set_query_var( 'term_id', $term_id );
347+
348+
// Simulate an ActivityPub request - should return early without redirect.
349+
$_SERVER['HTTP_ACCEPT'] = 'application/activity+json';
350+
351+
global $wp_query;
352+
353+
// Call template_redirect - it should return early for ActivityPub requests.
354+
Router::template_redirect();
355+
356+
// The query should not be set to 404 for valid category terms.
357+
$this->assertFalse( $wp_query->is_404(), 'Should not set 404 for valid category terms.' );
358+
235359
// Clean up.
236360
unset( $_SERVER['HTTP_ACCEPT'] );
361+
\set_query_var( 'term_id', null );
362+
\wp_delete_term( $term_id, 'category' );
363+
}
364+
365+
/**
366+
* Test that invalid term_id sets 404.
367+
*
368+
* @covers ::template_redirect
369+
*/
370+
public function test_invalid_term_id_sets_404() {
371+
// Set an invalid term_id query var.
372+
\set_query_var( 'term_id', 999999 );
373+
374+
global $wp_query;
375+
376+
// Call template_redirect - it should set 404 for invalid term.
377+
Router::template_redirect();
378+
379+
$this->assertTrue( $wp_query->is_404(), 'Should set 404 for invalid term_id.' );
380+
381+
// Clean up.
382+
\set_query_var( 'term_id', null );
383+
$wp_query->is_404 = false;
384+
}
385+
386+
/**
387+
* Test that supported taxonomy terms trigger redirects for non-ActivityPub requests.
388+
*
389+
* This verifies the core redirect functionality still works after the taxonomy filtering fix.
390+
* Uses an exception in the wp_redirect filter to intercept before exit() is called.
391+
*
392+
* @covers ::template_redirect
393+
*
394+
* @throws \Exception If a non-redirect exception is caught during template_redirect.
395+
*/
396+
public function test_supported_taxonomy_triggers_redirect() {
397+
// Create a category term.
398+
$term = \wp_insert_term( 'Redirect Test Category', 'category' );
399+
$this->assertNotWPError( $term, 'Term creation should succeed.' );
400+
401+
$term_id = $term['term_id'];
402+
$term_link = \get_term_link( $term_id, 'category' );
403+
404+
// Set the term_id query var.
405+
\set_query_var( 'term_id', $term_id );
406+
407+
// Save callback to variable for proper removal.
408+
$redirect_callback = function ( $location ) {
409+
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
410+
throw new \Exception( 'REDIRECT:' . $location );
411+
};
412+
413+
// Use exception to intercept redirect before exit() is called.
414+
\add_filter( 'wp_redirect', $redirect_callback );
415+
416+
$redirect_location = null;
417+
try {
418+
Router::template_redirect();
419+
} catch ( \Exception $e ) {
420+
if ( 0 === strpos( $e->getMessage(), 'REDIRECT:' ) ) {
421+
$redirect_location = substr( $e->getMessage(), 9 );
422+
} else {
423+
throw $e;
424+
}
425+
}
426+
427+
// Verify redirect was attempted to the correct term link.
428+
$this->assertNotNull( $redirect_location, 'Should attempt redirect for supported taxonomy term.' );
429+
$this->assertEquals( $term_link, $redirect_location, 'Should redirect to the term link.' );
430+
431+
// Clean up.
432+
\remove_filter( 'wp_redirect', $redirect_callback );
433+
\wp_delete_term( $term_id, 'category' );
434+
}
435+
436+
/**
437+
* Test that the activitypub_supported_taxonomies filter is actually used by the Router.
438+
*
439+
* This verifies that adding a custom taxonomy via the filter allows redirects for that taxonomy.
440+
*
441+
* @covers ::template_redirect
442+
*
443+
* @throws \Exception If a non-redirect exception is caught during template_redirect.
444+
*/
445+
public function test_filter_adds_custom_taxonomy_to_redirects() {
446+
// Register a custom taxonomy.
447+
\register_taxonomy(
448+
'custom_tax',
449+
'post',
450+
array(
451+
'public' => true,
452+
'label' => 'Custom Tax',
453+
)
454+
);
455+
456+
// Create a term in the custom taxonomy.
457+
$term = \wp_insert_term( 'Custom Term', 'custom_tax' );
458+
$this->assertNotWPError( $term, 'Term creation should succeed.' );
459+
460+
$term_id = $term['term_id'];
461+
$term_link = \get_term_link( $term_id, 'custom_tax' );
462+
463+
// Set the term_id query var.
464+
\set_query_var( 'term_id', $term_id );
465+
466+
// Save callbacks to variables for proper removal.
467+
$taxonomy_callback = function ( $taxonomies ) {
468+
$taxonomies[] = 'custom_tax';
469+
return $taxonomies;
470+
};
471+
$redirect_callback = function ( $location ) {
472+
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
473+
throw new \Exception( 'REDIRECT:' . $location );
474+
};
475+
476+
// Add custom taxonomy to supported list via filter.
477+
\add_filter( 'activitypub_supported_taxonomies', $taxonomy_callback );
478+
479+
// Use exception to intercept redirect before exit() is called.
480+
\add_filter( 'wp_redirect', $redirect_callback );
481+
482+
$redirect_location = null;
483+
try {
484+
Router::template_redirect();
485+
} catch ( \Exception $e ) {
486+
if ( 0 === strpos( $e->getMessage(), 'REDIRECT:' ) ) {
487+
$redirect_location = substr( $e->getMessage(), 9 );
488+
} else {
489+
throw $e;
490+
}
491+
}
492+
493+
// Verify redirect was attempted.
494+
$this->assertNotNull( $redirect_location, 'Should attempt redirect for custom taxonomy added via filter.' );
495+
$this->assertEquals( $term_link, $redirect_location, 'Should redirect to the custom taxonomy term link.' );
496+
497+
// Clean up.
498+
\remove_filter( 'wp_redirect', $redirect_callback );
499+
\remove_filter( 'activitypub_supported_taxonomies', $taxonomy_callback );
500+
\wp_delete_term( $term_id, 'custom_tax' );
501+
\unregister_taxonomy( 'custom_tax' );
237502
}
238503
}

0 commit comments

Comments
 (0)