Skip to content

8 [2/2]. Send the user back only to a screen the admin can serve - #80

Merged
nikolaystrikhar merged 2 commits into
mainfrom
61-redirector-screen-and-signature
Aug 25, 2026
Merged

8 [2/2]. Send the user back only to a screen the admin can serve#80
nikolaystrikhar merged 2 commits into
mainfrom
61-redirector-screen-and-signature

Conversation

@nikolaystrikhar

@nikolaystrikhar nikolaystrikhar commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What: Conflict\Redirector::after_deactivation() documents the string its one caller passes, and returns only a screen the current admin has a file for.

Usage: where an admin lands after a conflict is resolved.

Request URI Before After
/wp-admin/admin.php?page=give-settings that screen unchanged
/wp-login.php admin_url( 'wp-login.php' ) — a 404 plugins.php
/wp-content/plugins/give/give.php admin_url( 'give.php' ) — a 404 plugins.php
/wp-admin/edits.php (typo) admin_url( 'edits.php' ) — a 404 plugins.php
/wp-admin/network/options-general.php network_admin_url( … ) — a 404 network_admin_url( 'plugins.php' )

Why this way:

The type is a promise to callers, not a declaration. Resolver makes a string of $_SERVER['REQUEST_URI'] before it calls, so string is something analysis can hold callers to; declaring it would answer a host's filtered $_SERVER with a TypeError from inside plugins_loaded.

A well-formed basename is not a screen. The pattern admitted any *.php, so a URI naming one was rebuilt as an admin URL for a file that is not in wp-admin — a 404 on the request whose job is putting the admin back where they were.

Asked of the filesystem, not of a list of core's screens. A list would be wrong the first time a plugin registered a page: host screens are admin.php or edit.php with a page argument, so is_file() says yes to screens nobody has written yet. Asked in the admin the destination will be built in, since the network and user admins hold only core's files.

The pattern still runs first, and still with \z. It is what keeps a crafted name inside the directory being asked about; the anchor test stubs is_file() true so the anchor stays the only thing refusing a trailing newline.

Summary by CodeRabbit

  • Bug Fixes
    • Improved redirect handling after deactivation for site, network-admin, and user-admin screens.
    • Invalid, unsupported, empty, or unsafe request paths now fall back reliably instead of being treated as valid admin screens.
    • Added stricter validation for administrative page paths, including required file availability and trailing-newline handling.
    • Preserved correct redirects for supported plugin administration pages.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Enterprise

Run ID: 8a6f43cd-270c-4ab9-969d-0eb25f91ebf2

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab8e61 and 55d9835.

📒 Files selected for processing (2)
  • src/Conflict/Redirector.php
  • tests/unit/Conflict/RedirectorTest.php

📝 Walkthrough

Walkthrough

The redirector now requires string request URIs, validates PHP screen names against existing admin files, and handles site, network, and user admin contexts. Tests cover invalid inputs, unsupported paths, trailing newlines, and valid admin destinations.

Changes

Redirect validation

Layer / File(s) Summary
Screen extraction contract
src/Conflict/Redirector.php
after_deactivation() documents string request URIs. screen_from_path() validates anchored PHP screen names.
Admin file availability
src/Conflict/Redirector.php
is_admin_screen() checks the applicable network admin, user admin, or standard wp-admin directory.
Redirect behavior coverage
tests/unit/Conflict/RedirectorTest.php
Tests cover valid screens, unsupported values, invalid paths, trailing newlines, and admin-context-specific screens.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant Redirector
  participant AdminScreenCheck
  participant Filesystem
  Request->>Redirector: provide request URI
  Redirector->>AdminScreenCheck: extract and validate screen
  AdminScreenCheck->>Filesystem: check applicable admin file
  Filesystem-->>AdminScreenCheck: file exists or does not exist
  AdminScreenCheck-->>Redirector: valid screen or fallback
  Redirector-->>Request: selected redirect destination
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 61-redirector-screen-and-signature

Comment @coderabbitai help to get the list of available commands.

@nikolaystrikhar
nikolaystrikhar force-pushed the 61-redirector-screen-and-signature branch from 6e04ab7 to f2cc4c6 Compare August 25, 2026 08:44
@nikolaystrikhar nikolaystrikhar changed the title 16. Send the user back only to a screen the admin can serve 5B. Send the user back only to a screen the admin can serve Aug 25, 2026
@nikolaystrikhar nikolaystrikhar changed the title 5B. Send the user back only to a screen the admin can serve 3B. Send the user back only to a screen the admin can serve Aug 25, 2026
@nikolaystrikhar
nikolaystrikhar force-pushed the 61-redirector-screen-and-signature branch from f2cc4c6 to e607bcc Compare August 25, 2026 10:23
@nikolaystrikhar nikolaystrikhar changed the title 3B. Send the user back only to a screen the admin can serve 4 [2/2]. Send the user back only to a screen the admin can serve Aug 25, 2026
@nikolaystrikhar nikolaystrikhar changed the title 4 [2/2]. Send the user back only to a screen the admin can serve 8 [2/2]. Send the user back only to a screen the admin can serve Aug 25, 2026
Base automatically changed from 58-assertions-that-cannot-pass-wrongly to main August 25, 2026 12:57
…nothing

The docblock said `string|null`, the parameter was declared as nothing, the
guard refused anything that was not a non-empty string, and Resolver -- the one
caller -- had already made a string of $_SERVER['REQUEST_URI'] before it called.
Four answers to one question.

Narrowed the documented type to what the caller really passes rather than
widening it to what the guard tolerates. `string` is a promise callers can be
held to statically: Resolver's own narrowing is now checkable instead of
redundant, and a host reaching for this class is told what to hand over.
Widening the docblock to `mixed` would have thrown that away to describe a
runtime backstop, and declaring the type would have been worse still -- the
value comes from the SAPI and any plugin may have filtered $_SERVER, so under
strict_types a broken one would fatal from inside plugins_loaded, on the request
whose job is to get the admin back to a working screen. So the type is
documented and not declared, and the guard stays exactly as wide as it was.

The test that pinned the guard now runs over null, an array, an integer and a
boolean, since every one of those has to land on the plugins list rather than
raise. `null` moves out of the request-URI provider to join them.
The pattern said a basename was well formed, and the class read that as naming
an admin screen. `/wp-content/plugins/give/give.php`, `/wp-login.php` and
`/wp-admin/edits.php` all satisfy it, so each was rebuilt as an admin URL for a
file that is not in wp-admin -- an unstyled 404 from the web server, on the one
request whose job is to hand the admin back the screen they were on. The
documented answer for a URI naming no admin screen is the plugins list, and
that is what all three take now.

Asked of the filesystem, not of a list of core's screens. A list would be wrong
the first time a plugin registered a top-level page, and the class has to keep
working for a host's own screens: those are `admin.php`, `edit.php` or
`tools.php` with a `page` argument on them, so the file is core's however many
screens hang off it, and `is_file()` says yes to every one of them without
knowing any of their names. What it cannot tell apart is a screen from one of
the admin's own includes -- a browser is only ever on the first kind, and both
are inside wp-admin either way.

Asked of the admin the destination will be built in, matching admin_url_for()'s
three branches. The network and user admins hold only the files core gives
them, so a network request naming `options-general.php` was never on that
screen, and network_admin_url() would name the file that is missing.

The name still meets the pattern first, which is what keeps a crafted URI from
climbing out of the directory being asked about, and the `\z` anchor is still
the only thing refusing a trailing newline: the test for it stubs `is_file()`
true, since there is no "edit.php\n" on disk either and the anchor could
otherwise be taken out with nothing noticing.
@nikolaystrikhar
nikolaystrikhar force-pushed the 61-redirector-screen-and-signature branch from e607bcc to 55d9835 Compare August 25, 2026 12:57
@nikolaystrikhar
nikolaystrikhar merged commit dba4bde into main Aug 25, 2026
5 of 6 checks passed
@nikolaystrikhar
nikolaystrikhar deleted the 61-redirector-screen-and-signature branch August 25, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants