Skip to content

Every MediaWiki method that promises an absolute URL returns a relative one here #628

Closed

Description

@lens0021

Rewritten twice. First filed as an upstream report against WikiSEO — the wrong frame, nothing upstream is broken. Then rewritten as a three-way design choice whose answer was "keep patching each consumer". That was wrong too, and this is the version that holds: there is a fourth option, it is small, and #630 implements it.

The contract we break

Hooks\Main::onGetLocalURL rewrites every local URL to a file beside the one asking. That is what lets an export run from any directory and any host, and it is right.

But Title::getFullURL() and Title::getCanonicalURL() are both built on getLocalURL():

# getFullURL:      # Hand off all the decisions on urls to getLocalURL
                   $url = $this->getLocalURL( $query );
# getCanonicalURL: $url = $services->getUrlUtils()->expand( $this->getLocalURL( ... ), PROTO_CANONICAL );

So the hook does not only make page links relative. It makes every method MediaWiki offers for getting an absolute URL return a relative one. Anything that asks for a whole URL — core, an extension, a skin — gets a string that is not one, and has no way to know.

UrlUtils::expand() then makes it worse: ./index.html is a bare relative path, so removeDotSegments() strips the ./ and hands back index.html. A caller that prepends a scheme publishes http:index.html.

What it cost

The three options I first wrote down, and why all three were wrong

1. Keep the hook and patch each consumer. I argued this "fails safe": a missed URL is a relative link with the wrong depth, ugly but usually resolvable.

That was false, and #394 is the counterexample — "printfooter link 404s on any page exported into a subdirectory". A relative link at the wrong depth is a 404, not a graceful degradation. With the fail-safe claim gone, this is just an open-ended patch list.

2. Drop the hook; emit absolute URLs and rewrite them to relative in the HTML. Contracts stay intact and everything comes out right for free, but #444 and #425 show the rewriting layer leaks, and a leak here publishes http://localhost:4000/… to readers. Genuinely worse.

3. Narrow the hook with a re-entrancy flag so getLocalURL() behaves differently when called from getFullURL(). Fragile, and silently wrong in whichever direction it got wrong.

The fourth option, which is the answer

getFullURL() and getCanonicalURL() each run their own hook after calling getLocalURL() and expanding the result, and each hands it the same Title:

// includes/Title/Title.php
( new HookRunner( ... ) )->onGetFullURL( $this, $url, $query );
( new HookRunner( ... ) )->onGetCanonicalURL( $this, $url, $query );

That is option 3's goal without option 3's mechanism. Which of the three hooks ran is the signal that a caller wanted an absolute URL, and core delivers it — no flag to keep in sync, and nothing parsed back out of "http:index.html". The answer is recomputed from the title through the same OutputName that named the file, so it cannot drift from what the export actually wrote.

Body links stay relative, because getLocalURL() is still what renders them. Where WikvenSiteUrl is unset there is no address to give and the value is left alone.

Why it was not obvious

The three options were all framed as "what do we do about getLocalURL()", because that is the hook we own and the one the defect flows out of. The question worth asking instead was what else does core hand us at the point where the answer is already wrong — and the answer was two more hooks, forty lines away in the same file. Credit where due: Fable named the fourth option when asked whether one existed, and the source confirmed it.

The general lesson, worth keeping: when a hook we own has a bad effect on a caller, look for the hook nearest that caller before redesigning ours.

Beyond this

SiteUrl (#621) stays the single place that knows where the site is published; the hooks now go through it too, so a sitemap, an og:url and a future hreflang set all read one value.

Fixed by #630.

Activity

  1. added
    upstreamTracked here, but the fix belongs in another project
    on Sep 3, 2026
  2. changed the title [-]WikiSEO: og:url is built by prepending a scheme to whatever getFullURL returned, and OpenGraph cannot be switched off without losing Twitter cards too[/-] [+]Every MediaWiki method that promises an absolute URL returns a relative one here, and each consumer becomes a patch[/+] on Sep 3, 2026
  3. added
    enhancementNew feature or request
    and removed
    upstreamTracked here, but the fix belongs in another project
    on Sep 3, 2026
  4. changed the title [-]Every MediaWiki method that promises an absolute URL returns a relative one here, and each consumer becomes a patch[/-] [+]Every MediaWiki method that promises an absolute URL returns a relative one here[/+] on Sep 3, 2026
  5. added a commit that references this issue on Sep 3, 2026
    effaada
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions