Repository navigation
Every MediaWiki method that promises an absolute URL returns a relative one here #628
Description
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
- Core's
generateSitemap.phpemits relative<loc>values, which the sitemap protocol forbids. We wrote our own generator (feat: write a sitemap for a site that says where it is published #623). - WikiSEO's
og:urlcame out ashttp:index.htmlon every page (docs: install WikiSEO and claim the documentation site #627). Found by hand; all 24 checks were green while the broken tag shipped to the preview, because nothing asserted on it — the same shape as chore: repair the settings load order WikvenSiteUrl needs #624.
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
[-]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[/+][-]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[/+]fix: answer a caller that asked for a whole URL with one (#630)