Files
kate522andClaude Opus 5 e30de6562c fix(routing): bootstrap permalink structure so fresh installs resolve single posts
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>
2026-11-02 19:50:24 +08:00

126 lines
3.6 KiB
PHP

<?php
/**
* Behavioural harness for \Jio\Core\RewriteBootstrap.
*
* WordPress stubs only: an option bag, a WP_Rewrite stub recording what the theme asked of
* it, and an add_action/do_action pair so the hook wiring is exercised rather than assumed.
*/
namespace {
define('ABSPATH', __DIR__);
define('JIO_VERSION', '9.9.9-test');
$GLOBALS['jio_test_options'] = [
'permalink_structure' => '',
];
$GLOBALS['jio_test_set_permalink_calls'] = [];
$GLOBALS['jio_test_flush_calls'] = [];
$GLOBALS['jio_test_hooks'] = [];
}
namespace {
function jio_test_reset(array $options): void {
$GLOBALS['jio_test_options'] = $options;
$GLOBALS['jio_test_set_permalink_calls'] = [];
$GLOBALS['jio_test_flush_calls'] = [];
}
function get_option(string $key, $default = false) {
return array_key_exists($key, $GLOBALS['jio_test_options'])
? $GLOBALS['jio_test_options'][$key]
: $default;
}
function update_option(string $key, $value): bool {
$GLOBALS['jio_test_options'][$key] = $value;
return true;
}
function delete_option(string $key): bool {
unset($GLOBALS['jio_test_options'][$key]);
return true;
}
function add_action(string $hook, $callback, int $priority = 10, int $args = 1): void {
$GLOBALS['jio_test_hooks'][] = [
'hook' => $hook,
'priority' => $priority,
'method' => is_array($callback) ? (string) $callback[1] : (string) $callback,
];
}
function flush_rewrite_rules(bool $hard = true): void {
$GLOBALS['jio_test_flush_calls'][] = $hard;
}
}
namespace {
if (!class_exists('WP_Rewrite')) {
final class WP_Rewrite {
public array $structures = [];
public function set_permalink_structure(string $structure): void {
$this->structures[] = $structure;
// Core persists the option and refreshes in-memory state; the stub mirrors the
// persistence half so assertions on the option bag stay meaningful.
$GLOBALS['jio_test_options']['permalink_structure'] = $structure;
}
}
}
}
namespace {
require_once dirname(__DIR__, 2) . '/src/Core/RewriteBootstrap.php';
$bootstrap = \Jio\Core\RewriteBootstrap::get_instance();
$bootstrap->init();
$result = [
'hooks' => $GLOBALS['jio_test_hooks'],
];
// --- Case 1: fresh install, no permalink structure chosen yet.
jio_test_reset(['permalink_structure' => '']);
$wp_rewrite = new WP_Rewrite();
$GLOBALS['wp_rewrite'] = $wp_rewrite;
$bootstrap->ensure_permalink_structure();
$result['fresh_install'] = [
'option' => get_option('permalink_structure'),
'set_permalink' => $wp_rewrite->structures,
];
// --- Case 2: administrator already chose a structure — must not be overridden.
jio_test_reset(['permalink_structure' => '/%category%/%postname%.html']);
$wp_rewrite = new WP_Rewrite();
$GLOBALS['wp_rewrite'] = $wp_rewrite;
$bootstrap->ensure_permalink_structure();
$result['chosen_structure'] = [
'option' => get_option('permalink_structure'),
'set_permalink' => $wp_rewrite->structures,
];
// --- Case 3: version bump flushes once, then is a no-op.
jio_test_reset(['permalink_structure' => '/%postname%/']);
$bootstrap->flush_when_version_changes();
$first_flush = count($GLOBALS['jio_test_flush_calls']);
$stamp = get_option('jio_rewrite_flushed_version');
$bootstrap->flush_when_version_changes();
$result['version_flush'] = [
'flush_count' => $first_flush,
'stamp' => $stamp,
'repeat_flush' => count($GLOBALS['jio_test_flush_calls']) - $first_flush,
];
// --- Case 4: activation flush.
$bootstrap->flush_on_activate();
$result['activate'] = [
'flush_count' => count($GLOBALS['jio_test_flush_calls']),
];
echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
echo PHP_EOL;
}