A fresh install leaves `permalink_structure` empty, so WordPress snapshots
$wp_rewrite with no post/page permalink rules and every single post resolves
to 404 until someone re-saves Settings -> Permalinks.
The existing mitigation fired too late to help: routing.php wrote the option
and flushed from `init`, but WP_Rewrite was already constructed with the empty
structure, and core defers pre-`wp_loaded` flushes to a `wp_loaded` callback
that regenerates from that stale snapshot. The rewrite_rules option ended up
non-empty but broken, so the 404 persisted on every request.
New \Jio\Core\RewriteBootstrap owns this: on `after_setup_theme` (still inside
wp-settings.php, before wp-blog-header.php parses the query) it calls
$wp_rewrite->set_permalink_structure() — the same API wp-admin/options-permalink.php
uses — which persists the option *and* refreshes the in-memory snapshot, so both
the current request and the rules written for the next one are correct. A
structure an administrator chose is never overridden.
Also consolidates the two duplicated flush sites that lived inline in
functions.php (`after_switch_theme` closure and the `jio_flushed_version`
block, whose `delete_option('rewrite_rules')` forced a needless regenerate),
and drops the now-redundant permalink write from routing.php. The version
stamp moves to `wp_loaded` priority 20 so it sees the complete rule set
instead of the pre-`add_rewrite_rule()` snapshot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
5.3 KiB
JavaScript
110 lines
5.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const test = require('node:test');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
|
|
// A fresh WordPress install leaves `permalink_structure` empty, so WP_Rewrite is snapshotted
|
|
// with no post/page rules and every single post resolves to 404 until someone re-saves
|
|
// Settings → Permalinks. RewriteBootstrap must refresh that snapshot early enough to matter.
|
|
test('permalink structure is bootstrapped before the main query is parsed', () => {
|
|
const bootstrap = read('src/Core/RewriteBootstrap.php');
|
|
const functions = read('functions.php');
|
|
|
|
assert.match(bootstrap, /namespace Jio\\Core;/);
|
|
assert.match(bootstrap, /class RewriteBootstrap/);
|
|
assert.match(bootstrap, /const DEFAULT_STRUCTURE = '\/%postname%\/';/);
|
|
|
|
// The hook that makes the current request correct: after_setup_theme still runs inside
|
|
// wp-settings.php, before wp-blog-header.php parses the query.
|
|
assert.match(bootstrap, /add_action\('after_setup_theme', \[\$this, 'ensure_permalink_structure'\], 1\)/);
|
|
assert.match(bootstrap, /add_action\('after_switch_theme', \[\$this, 'flush_on_activate'\]\)/);
|
|
assert.match(bootstrap, /add_action\('wp_loaded', \[\$this, 'flush_when_version_changes'\], 20\)/);
|
|
|
|
// set_permalink_structure is what wp-admin/options-permalink.php uses: it persists the
|
|
// option AND refreshes the in-memory WP_Rewrite state for the current request.
|
|
assert.match(bootstrap, /\$wp_rewrite->set_permalink_structure\(self::DEFAULT_STRUCTURE\)/);
|
|
assert.match(bootstrap, /\$wp_rewrite instanceof \\WP_Rewrite/);
|
|
|
|
// Version bumps must not land on `init`: that runs before every add_rewrite_rule()
|
|
// callback, and core defers pre-wp_loaded flushes anyway.
|
|
assert.doesNotMatch(bootstrap, /add_action\('init'/);
|
|
assert.match(bootstrap, /get_option\('jio_rewrite_flushed_version'/);
|
|
|
|
// A structure the administrator chose is never overridden.
|
|
const ensure = bootstrap.slice(
|
|
bootstrap.indexOf('public function ensure_permalink_structure'),
|
|
bootstrap.indexOf('public function flush_on_activate'),
|
|
);
|
|
assert.match(ensure, /get_option\('permalink_structure'\)[\s\S]*?!== ''[\s\S]*?return;/);
|
|
});
|
|
|
|
test('functions.php delegates rewrite flushing to RewriteBootstrap', () => {
|
|
const functions = read('functions.php');
|
|
|
|
assert.match(functions, /\\Jio\\Core\\RewriteBootstrap::get_instance\(\)->init\(\);/);
|
|
// The old duplicated sites are gone.
|
|
assert.doesNotMatch(functions, /delete_option\('rewrite_rules'\)/);
|
|
assert.doesNotMatch(functions, /jio_flushed_version/);
|
|
assert.doesNotMatch(functions, /add_action\('after_switch_theme', function \(\) \{\s*flush_rewrite_rules\(\);/);
|
|
|
|
// RewriteBootstrap must be initialized before Setup, whose init registers the rewrite
|
|
// callbacks on the same request.
|
|
assert.ok(
|
|
functions.indexOf('\\Jio\\Core\\RewriteBootstrap::get_instance()->init();')
|
|
< functions.indexOf('\\Jio\\Core\\Setup::get_instance()->init();'),
|
|
'RewriteBootstrap must initialize before Setup',
|
|
);
|
|
});
|
|
|
|
test('user routing no longer writes permalink_structure itself', () => {
|
|
const routing = read('inc/modules/user-parts/routing.php');
|
|
|
|
// Single owner: the late init-time write is what made the first request 404 while leaving
|
|
// a non-empty but broken rewrite_rules option behind. (The read-only author_link fallback
|
|
// below is a separate, legitimate query-arg path.)
|
|
assert.doesNotMatch(routing, /update_option\('permalink_structure'/);
|
|
|
|
const flushCallback = routing.slice(
|
|
routing.indexOf('// 统一 flush 检查'),
|
|
routing.indexOf('if ($need_flush)'),
|
|
);
|
|
assert.doesNotMatch(flushCallback, /permalink_structure/);
|
|
|
|
// Its own message-routing version stamp stays — that is a separate self-heal concern.
|
|
assert.match(routing, /message-routing:' \. jio_get_messages_rewrite_slug\(\)/);
|
|
assert.match(routing, /flush_rewrite_rules\(true\)/);
|
|
});
|
|
|
|
test('fresh install gets a usable structure, chosen structures are left alone', () => {
|
|
const result = spawnSync(process.env.PHP_BINARY || 'php', [
|
|
path.join(__dirname, 'rewrite-bootstrap-harness.php'),
|
|
], { cwd: root, encoding: 'utf8' });
|
|
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
const data = JSON.parse(result.stdout);
|
|
|
|
// Hook wiring.
|
|
assert.deepEqual(data.hooks, [
|
|
{ hook: 'after_setup_theme', priority: 1, method: 'ensure_permalink_structure' },
|
|
{ hook: 'after_switch_theme', priority: 10, method: 'flush_on_activate' },
|
|
{ hook: 'wp_loaded', priority: 20, method: 'flush_when_version_changes' },
|
|
]);
|
|
|
|
// Fresh install: option written AND the in-memory WP_Rewrite snapshot refreshed.
|
|
assert.equal(data.fresh_install.option, '/%postname%/');
|
|
assert.deepEqual(data.fresh_install.set_permalink, ['/%postname%/']);
|
|
|
|
// An admin's own structure wins, untouched.
|
|
assert.equal(data.chosen_structure.option, '/%category%/%postname%.html');
|
|
assert.deepEqual(data.chosen_structure.set_permalink, []);
|
|
|
|
// Version stamp flushes exactly once, never per request.
|
|
assert.equal(data.version_flush.flush_count, 1);
|
|
assert.equal(data.version_flush.stamp, '9.9.9-test');
|
|
assert.equal(data.version_flush.repeat_flush, 0);
|
|
});
|