From 05967e192c23d0defa3fbe0d373fd3982c6ac941 Mon Sep 17 00:00:00 2001 From: Mike Auteri Date: Fri, 24 Jul 2026 18:13:08 -0400 Subject: [PATCH] Do not rewrite fragment-only URLs on import A reference like `#section` points within the document that contains it. It has no host and no path, so there is no origin to migrate. wp_rewrite_urls() resolved it against the base URL first, which made it look like a child of the site being imported from, and it was then rewritten against the new home: #section becomes /#section #add-to-calendar becomes /#add-to-calendar # becomes / (fragment dropped entirely) An in-page anchor therefore turns into a link to a different page, and blocks whose saved markup contains such an href fail validation after an import because the serialized attribute no longer matches what the block expects. Sites have been working around this by disabling URL rewriting altogether via the wp_import_options filter, which is a heavy price for one class of URL. Skips those references before the mapping is consulted. The guard reads the raw URL rather than the parsed one, because parsing is what loses the distinction: by the time a fragment has been resolved against the base it is indistinguishable from a same-site link. Scoped deliberately to references that are *only* a fragment. A URL that carries a fragment alongside a host or path is still rewritten, and still keeps its fragment. Adds regression tests covering fragment-only hrefs in markup and in block attributes, plus the two cases that must keep working. Co-Authored-By: Claude Opus 5 --- phpunit/tests/rewrite-urls.php | 94 +++++++++++++++++++ .../DataLiberation/URL/functions.php | 13 +++ 2 files changed, 107 insertions(+) create mode 100644 phpunit/tests/rewrite-urls.php diff --git a/phpunit/tests/rewrite-urls.php b/phpunit/tests/rewrite-urls.php new file mode 100644 index 00000000..7a7360be --- /dev/null +++ b/phpunit/tests/rewrite-urls.php @@ -0,0 +1,94 @@ + $block_markup, + 'url-mapping' => array( self::FROM => self::TO ), + 'base_url' => self::FROM, + ) + ); + } + + /** + * Fragment-only references point within the document that contains them, + * so there is no origin to migrate and nothing to rewrite. + * + * Rewriting them resolves the fragment against the base URL first, which + * turns an in-page anchor into a link to a different page: `#section` + * became `/#section` and a bare `#` became `/`, dropping the fragment + * altogether. Blocks whose saved markup contains such an href then fail + * validation after an import. + * + * @covers ::WordPress\DataLiberation\URL\wp_rewrite_urls + * + * @dataProvider data_fragment_only_urls + * + * @param string $block_markup Markup containing a fragment-only reference. + */ + public function test_does_not_rewrite_fragment_only_urls( $block_markup ) { + $this->assertSame( + $block_markup, + $this->rewrite( $block_markup ), + 'Fragment-only references must survive a rewrite untouched.' + ); + } + + /** + * Data provider for fragment-only references. + * + * @return array> + */ + public function data_fragment_only_urls() { + return array( + 'named fragment' => array( 'Jump' ), + 'bare hash' => array( 'Placeholder' ), + 'hyphenated fragment' => array( 'iCal' ), + 'fragment in a block attribute' => array( '' ), + ); + } + + /** + * The guard is limited to references that are *only* a fragment. A real + * URL keeps being rewritten, and keeps its fragment while doing so. + * + * @covers ::WordPress\DataLiberation\URL\wp_rewrite_urls + */ + public function test_still_rewrites_absolute_urls_carrying_a_fragment() { + $this->assertSame( + 'Deep link', + $this->rewrite( 'Deep link' ) + ); + } + + /** + * Ordinary rewriting is unaffected. + * + * @covers ::WordPress\DataLiberation\URL\wp_rewrite_urls + */ + public function test_still_rewrites_absolute_urls() { + $this->assertSame( + 'Page', + $this->rewrite( 'Page' ) + ); + } +} diff --git a/src/php-toolkit/DataLiberation/URL/functions.php b/src/php-toolkit/DataLiberation/URL/functions.php index 18d793a2..0be93dc4 100644 --- a/src/php-toolkit/DataLiberation/URL/functions.php +++ b/src/php-toolkit/DataLiberation/URL/functions.php @@ -53,6 +53,19 @@ function wp_rewrite_urls( $options ) { $p = new BlockMarkupUrlProcessor( $options['block_markup'], $options['base_url'] ); while ( $p->next_url() ) { + /* + * Leave fragment-only references alone. A URL like `#section` points + * within the document that contains it, so there is no origin to + * migrate. Resolving it against the base URL makes it look like a + * child of the site being imported from, and rewriting it then turns + * an in-page anchor into a link somewhere else entirely: + * `#section` becomes `/#section`, and a bare `#` becomes `/`. + */ + $raw_url = $p->get_raw_url(); + if ( is_string( $raw_url ) && 0 === strpos( $raw_url, '#' ) ) { + continue; + } + $parsed_url = $p->get_parsed_url(); foreach ( $url_mapping as $mapping ) { if ( is_child_url_of( $parsed_url, $mapping['from_url'] ) ) {