Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.81% covered (warning)
59.81%
189 / 316
33.33% covered (danger)
33.33%
5 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActionEntryPoint
59.81% covered (warning)
59.81%
189 / 316
33.33% covered (danger)
33.33%
5 / 15
1209.26
0.00% covered (danger)
0.00%
0 / 1
 getContext
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getOutput
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handleTopLevelError
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 execute
59.38% covered (warning)
59.38%
19 / 32
0.00% covered (danger)
0.00%
0 / 1
12.29
 maybeDoHttpsRedirect
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
8.86
 doPrepareForOutput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 schedulePostSendJobs
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 parseTitle
95.65% covered (success)
95.65%
44 / 46
0.00% covered (danger)
0.00%
0 / 1
29
 getTitle
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 getAction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 performRequest
59.04% covered (warning)
59.04%
49 / 83
0.00% covered (danger)
0.00%
0 / 1
77.11
 tryNormaliseRedirect
46.15% covered (danger)
46.15%
18 / 39
0.00% covered (danger)
0.00%
0 / 1
39.38
 initializeArticle
56.76% covered (warning)
56.76%
21 / 37
0.00% covered (danger)
0.00%
0 / 1
61.14
 performAction
65.71% covered (warning)
65.71%
23 / 35
0.00% covered (danger)
0.00%
0 / 1
14.03
1<?php
2
3namespace MediaWiki\Actions;
4
5use Action;
6use Article;
7use BadTitleError;
8use ErrorPageError;
9use HTMLFileCache;
10use HttpError;
11use MediaWiki\Context\RequestContext;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\MainConfigNames;
14use MediaWiki\MediaWikiEntryPoint;
15use MediaWiki\Output\OutputPage;
16use MediaWiki\Permissions\PermissionStatus;
17use MediaWiki\Profiler\ProfilingContext;
18use MediaWiki\Request\DerivativeRequest;
19use MediaWiki\Request\WebRequest;
20use MediaWiki\SpecialPage\RedirectSpecialPage;
21use MediaWiki\SpecialPage\SpecialPage;
22use MediaWiki\Title\MalformedTitleException;
23use MediaWiki\Title\Title;
24use MediaWiki\User\User;
25use MWExceptionRenderer;
26use PermissionsError;
27use Profiler;
28use Throwable;
29use UnexpectedValueException;
30use ViewAction;
31use WikiFilePage;
32use Wikimedia\Rdbms\DBConnectionError;
33
34/**
35 * The index.php entry point for web browser navigations, usually routed to
36 * an Action or SpecialPage subclass.
37 *
38 * @internal For use in index.php
39 * @ingroup entrypoint
40 */
41class ActionEntryPoint extends MediaWikiEntryPoint {
42
43    /**
44     * Overwritten to narrow the return type to RequestContext
45     * @return RequestContext
46     */
47    protected function getContext(): RequestContext {
48        /** @var RequestContext $context */
49        $context = parent::getContext();
50
51        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType see $context in the constructor
52        return $context;
53    }
54
55    protected function getOutput(): OutputPage {
56        return $this->getContext()->getOutput();
57    }
58
59    protected function getUser(): User {
60        return $this->getContext()->getUser();
61    }
62
63    protected function handleTopLevelError( Throwable $e ) {
64        $context = $this->getContext();
65        $action = $context->getRequest()->getRawVal( 'action', 'view' );
66        if (
67            $e instanceof DBConnectionError &&
68            $context->hasTitle() &&
69            $context->getTitle()->canExist() &&
70            in_array( $action, [ 'view', 'history' ], true ) &&
71            HTMLFileCache::useFileCache( $context, HTMLFileCache::MODE_OUTAGE )
72        ) {
73            // Try to use any (even stale) file during outages...
74            $cache = new HTMLFileCache( $context->getTitle(), $action );
75            if ( $cache->isCached() ) {
76                $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE );
77                $this->print( MWExceptionRenderer::getHTML( $e ) );
78                $this->exit();
79            }
80        }
81
82        parent::handleTopLevelError( $e );
83    }
84
85    /**
86     * Determine and send the response headers and body for this web request
87     */
88    protected function execute() {
89        global $wgTitle;
90
91        // Get title from request parameters,
92        // is set on the fly by parseTitle the first time.
93        $title = $this->getTitle();
94        $wgTitle = $title;
95
96        $request = $this->getContext()->getRequest();
97        // Set DB query expectations for this HTTP request
98        $trxLimits = $this->getConfig( MainConfigNames::TrxProfilerLimits );
99        $trxProfiler = Profiler::instance()->getTransactionProfiler();
100        $trxProfiler->setLogger( LoggerFactory::getInstance( 'rdbms' ) );
101        $trxProfiler->setStatsdDataFactory( $this->getStatsdDataFactory() );
102        $trxProfiler->setRequestMethod( $request->getMethod() );
103        if ( $request->hasSafeMethod() ) {
104            $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
105        } else {
106            $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
107        }
108
109        if ( $this->maybeDoHttpsRedirect() ) {
110            return;
111        }
112
113        $context = $this->getContext();
114        $output = $context->getOutput();
115
116        // NOTE: HTMLFileCache::useFileCache() is not used in WMF production but is
117        //       here to provide third-party wikis with a way to enable caching for
118        //       "view" and "history" actions. It's triggered by the use of $wgUseFileCache
119        //       when set to true in LocalSettings.php.
120        if ( $title->canExist() && HTMLFileCache::useFileCache( $context ) ) {
121            // getAction() may trigger DB queries, so avoid eagerly initializing it if possible.
122            // This reduces the cost of requests that exit early due to tryNormaliseRedirect()
123            // or a MediaWikiPerformAction / BeforeInitialize hook handler.
124            $action = $this->getAction();
125            // Try low-level file cache hit
126            $cache = new HTMLFileCache( $title, $action );
127            if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
128                // Check incoming headers to see if client has this cached
129                $timestamp = $cache->cacheTimestamp();
130                if ( !$output->checkLastModified( $timestamp ) ) {
131                    $cache->loadFromFileCache( $context );
132                }
133                // Do any stats increment/watchlist stuff, assuming user is viewing the
134                // latest revision (which should always be the case for file cache)
135                $context->getWikiPage()->doViewUpdates( $context->getAuthority() );
136                // Tell OutputPage that output is taken care of
137                $output->disable();
138
139                return;
140            }
141        }
142
143        try {
144            // Actually do the work of the request and build up any output
145            $this->performRequest();
146        } catch ( ErrorPageError $e ) {
147            // TODO: Should ErrorPageError::report accept a OutputPage parameter?
148            $e->report( ErrorPageError::STAGE_OUTPUT );
149            $output->considerCacheSettingsFinal();
150            // T64091: while exceptions are convenient to bubble up GUI errors,
151            // they are not internal application faults. As with normal requests, this
152            // should commit, print the output, do deferred updates, jobs, and profiling.
153        }
154
155        $this->prepareForOutput();
156
157        // Ask OutputPage/Skin to stage the output (HTTP response body and headers).
158        // Flush the output to the client unless an exception occurred.
159        // Note that the OutputPage object in $context may have been replaced,
160        // so better fetch it again here.
161        $output = $context->getOutput();
162        $this->outputResponsePayload( $output->output( true ) );
163    }
164
165    /**
166     * If the stars are suitably aligned, do an HTTP->HTTPS redirect
167     *
168     * Note: Do this after $wgTitle is setup, otherwise the hooks run from
169     * isRegistered() will do all sorts of weird stuff.
170     *
171     * @return bool True if the redirect was done. Handling of the request
172     *   should be aborted. False if no redirect was done.
173     */
174    protected function maybeDoHttpsRedirect() {
175        if ( !$this->shouldDoHttpRedirect() ) {
176            return false;
177        }
178
179        $context = $this->getContext();
180        $request = $context->getRequest();
181        $oldUrl = $request->getFullRequestURL();
182        $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
183
184        if ( $request->wasPosted() ) {
185            // This is weird and we'd hope it almost never happens. This
186            // means that a POST came in via HTTP and policy requires us
187            // redirecting to HTTPS. It's likely such a request is going
188            // to fail due to post data being lost, but let's try anyway
189            // and just log the instance.
190
191            // @todo FIXME: See if we could issue a 307 or 308 here, need
192            // to see how clients (automated & browser) behave when we do
193            wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
194        }
195        // Setup dummy Title, otherwise OutputPage::redirect will fail
196        $title = Title::newFromText( 'REDIR', NS_MAIN );
197        $context->setTitle( $title );
198        // Since we only do this redir to change proto, always send a vary header
199        $output = $context->getOutput();
200        $output->addVaryHeader( 'X-Forwarded-Proto' );
201        $output->redirect( $redirUrl );
202        $output->output();
203
204        return true;
205    }
206
207    protected function doPrepareForOutput() {
208        parent::doPrepareForOutput();
209
210        // If needed, push a deferred update to run jobs after the output is sent
211        $this->schedulePostSendJobs();
212    }
213
214    protected function schedulePostSendJobs() {
215        // Recursion guard for $wgRunJobsAsync
216        if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
217            return;
218        }
219
220        parent::schedulePostSendJobs();
221    }
222
223    /**
224     * Parse the request to get the Title object
225     *
226     * @throws MalformedTitleException If a title has been provided by the user, but is invalid.
227     * @param WebRequest $request
228     * @return Title Title object to be $wgTitle
229     */
230    protected function parseTitle( $request ) {
231        $curid = $request->getInt( 'curid' );
232        $title = $request->getText( 'title' );
233
234        $ret = null;
235        if ( $curid ) {
236            // URLs like this are generated by RC, because rc_title isn't always accurate
237            $ret = Title::newFromID( $curid );
238        }
239        if ( $ret === null ) {
240            $ret = Title::newFromURL( $title );
241            if ( $ret !== null ) {
242                // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
243                // in wikitext links to tell Parser to make a direct file link
244                if ( $ret->getNamespace() === NS_MEDIA ) {
245                    $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() );
246                }
247                // Check variant links so that interwiki links don't have to worry
248                // about the possible different language variants
249                $services = $this->getServiceContainer();
250                $languageConverter = $services
251                    ->getLanguageConverterFactory()
252                    ->getLanguageConverter( $services->getContentLanguage() );
253                if ( $languageConverter->hasVariants() && !$ret->exists() ) {
254                    $languageConverter->findVariantLink( $title, $ret );
255                }
256            }
257        }
258
259        // If title is not provided, always allow oldid and diff to set the title.
260        // If title is provided, allow oldid and diff to override the title, unless
261        // we are talking about a special page which might use these parameters for
262        // other purposes.
263        if ( $ret === null || !$ret->isSpecialPage() ) {
264            // We can have urls with just ?diff=,?oldid= or even just ?diff=
265            $oldid = $request->getInt( 'oldid' );
266            $oldid = $oldid ?: $request->getInt( 'diff' );
267            // Allow oldid to override a changed or missing title
268            if ( $oldid ) {
269                $revRecord = $this->getServiceContainer()
270                    ->getRevisionLookup()
271                    ->getRevisionById( $oldid );
272                if ( $revRecord ) {
273                    $ret = Title::newFromLinkTarget(
274                        $revRecord->getPageAsLinkTarget()
275                    );
276                }
277            }
278        }
279
280        if ( $ret === null && $request->getCheck( 'search' ) ) {
281            // Compatibility with old search URLs which didn't use Special:Search
282            // Just check for presence here, so blank requests still
283            // show the search page when using ugly URLs (T10054).
284            $ret = SpecialPage::getTitleFor( 'Search' );
285        }
286
287        if ( $ret === null || !$ret->isSpecialPage() ) {
288            // Compatibility with old URLs for Special:RevisionDelete/Special:EditTags (T323338)
289            $actionName = $request->getRawVal( 'action' );
290            if (
291                $actionName === 'revisiondelete' ||
292                ( $actionName === 'historysubmit' && $request->getBool( 'revisiondelete' ) )
293            ) {
294                $ret = SpecialPage::getTitleFor( 'Revisiondelete' );
295            } elseif (
296                $actionName === 'editchangetags' ||
297                ( $actionName === 'historysubmit' && $request->getBool( 'editchangetags' ) )
298            ) {
299                $ret = SpecialPage::getTitleFor( 'EditTags' );
300            }
301        }
302
303        // Use the main page as default title if nothing else has been provided
304        if ( $ret === null
305            && strval( $title ) === ''
306            && !$request->getCheck( 'curid' )
307            && $request->getRawVal( 'action' ) !== 'delete'
308        ) {
309            $ret = Title::newMainPage();
310        }
311
312        if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) {
313            // If we get here, we definitely don't have a valid title; throw an exception.
314            // Try to get detailed invalid title exception first, fall back to MalformedTitleException.
315            Title::newFromTextThrow( $title );
316            throw new MalformedTitleException( 'badtitletext', $title );
317        }
318
319        return $ret;
320    }
321
322    /**
323     * Get the Title object that we'll be acting on, as specified in the WebRequest
324     * @return Title
325     */
326    public function getTitle() {
327        $context = $this->getContext();
328
329        if ( !$context->hasTitle() ) {
330            try {
331                $context->setTitle( $this->parseTitle( $context->getRequest() ) );
332            } catch ( MalformedTitleException $ex ) {
333                $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
334            }
335        }
336        return $context->getTitle();
337    }
338
339    /**
340     * Returns the name of the action that will be executed.
341     *
342     * @note This is public for the benefit of extensions that implement
343     * the BeforeInitialize or MediaWikiPerformAction hooks.
344     *
345     * @return string Action
346     */
347    public function getAction(): string {
348        return $this->getContext()->getActionName();
349    }
350
351    /**
352     * Performs the request.
353     * - bad titles
354     * - read restriction
355     * - local interwiki redirects
356     * - redirect loop
357     * - special pages
358     * - normal pages
359     *
360     * @throws PermissionsError|BadTitleError|HttpError
361     * @return void
362     */
363    protected function performRequest() {
364        global $wgTitle;
365
366        $context = $this->getContext();
367
368        $request = $context->getRequest();
369        $output = $context->getOutput();
370
371        if ( $request->getRawVal( 'printable' ) === 'yes' ) {
372            $output->setPrintable();
373        }
374
375        $user = $context->getUser();
376        $title = $context->getTitle();
377        $requestTitle = $title;
378
379        $userOptionsLookup = $this->getServiceContainer()->getUserOptionsLookup();
380        if ( $userOptionsLookup->getBoolOption( $user, 'forcesafemode' ) ) {
381            $request->setVal( 'safemode', '1' );
382        }
383
384        $this->getHookRunner()->onBeforeInitialize( $title, null, $output, $user, $request, $this );
385
386        // Invalid titles. T23776: The interwikis must redirect even if the page name is empty.
387        if ( $title === null || ( $title->getDBkey() == '' && !$title->isExternal() )
388            || $title->isSpecial( 'Badtitle' )
389        ) {
390            $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
391            try {
392                $this->parseTitle( $request );
393            } catch ( MalformedTitleException $ex ) {
394                throw new BadTitleError( $ex );
395            }
396            throw new BadTitleError();
397        }
398
399        // Check user's permissions to read this page.
400        // We have to check here to catch special pages etc.
401        // We will check again in Article::view().
402        $permissionStatus = PermissionStatus::newEmpty();
403        if ( !$context->getAuthority()->authorizeRead( 'read', $title, $permissionStatus ) ) {
404            // T34276: allowing the skin to generate output with $wgTitle or
405            // $context->title set to the input title would allow anonymous users to
406            // determine whether a page exists, potentially leaking private data. In fact, the
407            // curid and oldid request  parameters would allow page titles to be enumerated even
408            // when they are not guessable. So we reset the title to Special:Badtitle before the
409            // permissions error is displayed.
410
411            // The skin mostly uses $context->getTitle() these days, but some extensions
412            // still use $wgTitle.
413            $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
414            $context->setTitle( $badTitle );
415            $wgTitle = $badTitle;
416
417            throw new PermissionsError( 'read', $permissionStatus );
418        }
419
420        // Interwiki redirects
421        if ( $title->isExternal() ) {
422            $rdfrom = $request->getVal( 'rdfrom' );
423            if ( $rdfrom ) {
424                $url = $title->getFullURL( [ 'rdfrom' => $rdfrom ] );
425            } else {
426                $query = $request->getValues();
427                unset( $query['title'] );
428                $url = $title->getFullURL( $query );
429            }
430            // Check for a redirect loop
431            if ( $url !== $request->getFullRequestURL() && $title->isLocal() ) {
432                // 301 so google et al report the target as the actual url.
433                $output->redirect( $url, 301 );
434            } else {
435                $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
436                try {
437                    $this->parseTitle( $request );
438                } catch ( MalformedTitleException $ex ) {
439                    throw new BadTitleError( $ex );
440                }
441                throw new BadTitleError();
442            }
443            // Handle any other redirects.
444            // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
445        } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
446            // Prevent information leak via Special:MyPage et al (T109724)
447            $spFactory = $this->getServiceContainer()->getSpecialPageFactory();
448            if ( $title->isSpecialPage() ) {
449                $specialPage = $spFactory->getPage( $title->getDBkey() );
450                if ( $specialPage instanceof RedirectSpecialPage ) {
451                    $specialPage->setContext( $context );
452                    if ( $this->getConfig( MainConfigNames::HideIdentifiableRedirects )
453                        && $specialPage->personallyIdentifiableTarget()
454                    ) {
455                        [ , $subpage ] = $spFactory->resolveAlias( $title->getDBkey() );
456                        $target = $specialPage->getRedirect( $subpage );
457                        // Target can also be true. We let that case fall through to normal processing.
458                        if ( $target instanceof Title ) {
459                            if ( $target->isExternal() ) {
460                                // Handle interwiki redirects
461                                $target = SpecialPage::getTitleFor(
462                                    'GoToInterwiki',
463                                    'force/' . $target->getPrefixedDBkey()
464                                );
465                            }
466
467                            $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
468                            $derivateRequest = new DerivativeRequest( $request, $query );
469                            $derivateRequest->setRequestURL( $request->getRequestURL() );
470                            $context->setRequest( $derivateRequest );
471                            // Do not varnish cache these. May vary even for anons
472                            $output->lowerCdnMaxage( 0 );
473                            // NOTE: This also clears any action cache.
474                            // Action should not have been computed yet, but if it was,
475                            // we reset it because special pages only support "view".
476                            $context->setTitle( $target );
477                            $wgTitle = $target;
478                            $title = $target;
479                            $output->addJsConfigVars( [
480                                'wgInternalRedirectTargetUrl' => $target->getLinkURL( $query ),
481                            ] );
482                            $output->addModules( 'mediawiki.action.view.redirect' );
483
484                            // If the title is invalid, redirect but show the correct bad title error - T297407
485                            if ( !$target->isValid() ) {
486                                try {
487                                    $this->getServiceContainer()->getTitleParser()
488                                        ->parseTitle( $target->getPrefixedText() );
489                                } catch ( MalformedTitleException $ex ) {
490                                    throw new BadTitleError( $ex );
491                                }
492                                throw new BadTitleError();
493                            }
494                        }
495                    }
496                }
497            }
498
499            // Special pages ($title may have changed since if statement above)
500            if ( $title->isSpecialPage() ) {
501                // Actions that need to be made when we have a special pages
502                $spFactory->executePath( $title, $context );
503            } else {
504                // ...otherwise treat it as an article view. The article
505                // may still be a wikipage redirect to another article or URL.
506                $article = $this->initializeArticle();
507                if ( is_object( $article ) ) {
508                    $this->performAction( $article, $requestTitle );
509                } elseif ( is_string( $article ) ) {
510                    $output->redirect( $article );
511                } else {
512                    throw new UnexpectedValueException( "Shouldn't happen: MediaWiki::initializeArticle()"
513                        . " returned neither an object nor a URL" );
514                }
515            }
516            $output->considerCacheSettingsFinal();
517        }
518    }
519
520    /**
521     * Handle redirects for uncanonical title requests.
522     *
523     * Handles:
524     * - Redirect loops.
525     * - No title in URL.
526     * - $wgUsePathInfo URLs.
527     * - URLs with a variant.
528     * - Other non-standard URLs (as long as they have no extra query parameters).
529     *
530     * Behaviour:
531     * - Normalise title values:
532     *   /wiki/Foo%20Bar -> /wiki/Foo_Bar
533     * - Normalise empty title:
534     *   /wiki/ -> /wiki/Main
535     *   /w/index.php?title= -> /wiki/Main
536     * - Don't redirect anything with query parameters other than 'title' or 'action=view'.
537     *
538     * @param Title $title
539     * @return bool True if a redirect was set.
540     * @throws HttpError
541     */
542    protected function tryNormaliseRedirect( Title $title ): bool {
543        $request = $this->getRequest();
544        $output = $this->getOutput();
545
546        if ( $request->getRawVal( 'action', 'view' ) != 'view'
547            || $request->wasPosted()
548            || ( $request->getCheck( 'title' )
549                && $title->getPrefixedDBkey() == $request->getText( 'title' ) )
550            || count( $request->getValueNames( [ 'action', 'title' ] ) )
551            || !$this->getHookRunner()->onTestCanonicalRedirect( $request, $title, $output )
552        ) {
553            return false;
554        }
555
556        if ( $this->getConfig( MainConfigNames::MainPageIsDomainRoot ) && $request->getRequestURL() === '/' ) {
557            return false;
558        }
559
560        $services = $this->getServiceContainer();
561
562        if ( $title->isSpecialPage() ) {
563            [ $name, $subpage ] = $services->getSpecialPageFactory()
564                ->resolveAlias( $title->getDBkey() );
565
566            if ( $name ) {
567                $title = SpecialPage::getTitleFor( $name, $subpage );
568            }
569        }
570        // Redirect to canonical url, make it a 301 to allow caching
571        $targetUrl = (string)$services->getUrlUtils()->expand( $title->getFullURL(), PROTO_CURRENT );
572        if ( $targetUrl == $request->getFullRequestURL() ) {
573            $message = "Redirect loop detected!\n\n" .
574                "This means the wiki got confused about what page was " .
575                "requested; this sometimes happens when moving a wiki " .
576                "to a new server or changing the server configuration.\n\n";
577
578            if ( $this->getConfig( MainConfigNames::UsePathInfo ) ) {
579                $message .= "The wiki is trying to interpret the page " .
580                    "title from the URL path portion (PATH_INFO), which " .
581                    "sometimes fails depending on the web server. Try " .
582                    "setting \"\$wgUsePathInfo = false;\" in your " .
583                    "LocalSettings.php, or check that \$wgArticlePath " .
584                    "is correct.";
585            } else {
586                $message .= "Your web server was detected as possibly not " .
587                    "supporting URL path components (PATH_INFO) correctly; " .
588                    "check your LocalSettings.php for a customized " .
589                    "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
590                    "to true.";
591            }
592            throw new HttpError( 500, $message );
593        }
594        $output->setCdnMaxage( 1200 );
595        $output->redirect( $targetUrl, '301' );
596        return true;
597    }
598
599    /**
600     * Initialize the main Article object for "standard" actions (view, etc)
601     * Create an Article object for the page, following redirects if needed.
602     *
603     * @return Article|string An Article, or a string to redirect to another URL
604     */
605    protected function initializeArticle() {
606        $context = $this->getContext();
607
608        $title = $context->getTitle();
609        $services = $this->getServiceContainer();
610        if ( $context->canUseWikiPage() ) {
611            // Optimization: Reuse the WikiPage instance from context, to avoid
612            // repeat fetching or computation of data already loaded.
613            $page = $context->getWikiPage();
614        } else {
615            // This case should not happen, but just in case.
616            // @TODO: remove this or use an exception
617            $page = $services->getWikiPageFactory()->newFromTitle( $title );
618            $context->setWikiPage( $page );
619            wfWarn( "RequestContext::canUseWikiPage() returned false" );
620        }
621
622        // Make GUI wrapper for the WikiPage
623        $article = Article::newFromWikiPage( $page, $context );
624
625        // Skip some unnecessary code if the content model doesn't support redirects
626        // Use the page content model rather than invoking Title::getContentModel()
627        // to avoid querying page data twice (T206498)
628        if ( !$page->getContentHandler()->supportsRedirects() ) {
629            return $article;
630        }
631
632        $request = $context->getRequest();
633
634        // Namespace might change when using redirects
635        // Check for redirects ...
636        $action = $request->getRawVal( 'action', 'view' );
637        $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null;
638        if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
639            && !$request->getCheck( 'oldid' ) // ... and are not old revisions
640            && !$request->getCheck( 'diff' ) // ... and not when showing diff
641            && $request->getRawVal( 'redirect' ) !== 'no' // ... unless explicitly told not to
642            // ... and the article is not a non-redirect image page with associated file
643            && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
644        ) {
645            // Give extensions a change to ignore/handle redirects as needed
646            $ignoreRedirect = $target = false;
647
648            $this->getHookRunner()->onInitializeArticleMaybeRedirect( $title, $request,
649                // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
650                $ignoreRedirect, $target, $article );
651            $page = $article->getPage(); // reflect any hook changes
652
653            // Follow redirects only for... redirects.
654            // If $target is set, then a hook wanted to redirect.
655            if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
656                // Is the target already set by an extension?
657                $target = $target ?: $page->followRedirect();
658                if ( is_string( $target ) && !$this->getConfig( MainConfigNames::DisableHardRedirects ) ) {
659                    // we'll need to redirect
660                    return $target;
661                }
662                if ( is_object( $target ) ) {
663                    // Rewrite environment to redirected article
664                    $rpage = $services->getWikiPageFactory()->newFromTitle( $target );
665                    $rpage->loadPageData();
666                    if ( $rpage->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
667                        $rarticle = Article::newFromWikiPage( $rpage, $context );
668                        $rarticle->setRedirectedFrom( $title );
669
670                        $article = $rarticle;
671                        // NOTE: This also clears any action cache
672                        $context->setTitle( $target );
673                        $context->setWikiPage( $article->getPage() );
674                    }
675                }
676            }
677        }
678
679        return $article;
680    }
681
682    /**
683     * Perform one of the "standard" actions
684     *
685     * @param Article $article
686     * @param Title $requestTitle The original title, before any redirects were applied
687     */
688    protected function performAction( Article $article, Title $requestTitle ) {
689        $request = $this->getRequest();
690        $output = $this->getOutput();
691        $title = $this->getTitle();
692        $user = $this->getUser();
693
694        if ( !$this->getHookRunner()->onMediaWikiPerformAction(
695            $output, $article, $title, $user, $request, $this )
696        ) {
697            return;
698        }
699
700        $t = microtime( true );
701        $actionName = $this->getAction();
702        $services = $this->getServiceContainer();
703
704        $action = $services->getActionFactory()->getAction( $actionName, $article, $this->getContext() );
705        if ( $action instanceof Action ) {
706            ProfilingContext::singleton()->init( MW_ENTRY_POINT, $actionName );
707
708            // Check read permissions
709            if ( $action->needsReadRights() && !$user->isAllowed( 'read' ) ) {
710                throw new PermissionsError( 'read' );
711            }
712
713            // Narrow DB query expectations for this HTTP request
714            if ( $request->wasPosted() && !$action->doesWrites() ) {
715                $trxProfiler = Profiler::instance()->getTransactionProfiler();
716                $trxLimits = $this->getConfig( MainConfigNames::TrxProfilerLimits );
717                $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
718            }
719
720            // Let CDN cache things if we can purge them.
721            // Also unconditionally cache page views.
722            if ( $this->getConfig( MainConfigNames::UseCdn ) ) {
723                $htmlCacheUpdater = $services->getHtmlCacheUpdater();
724                if ( $request->matchURLForCDN( $htmlCacheUpdater->getUrls( $requestTitle ) ) ) {
725                    $output->setCdnMaxage( $this->getConfig( MainConfigNames::CdnMaxAge ) );
726                } elseif ( $action instanceof ViewAction ) {
727                    $output->setCdnMaxage( 3600 );
728                }
729            }
730
731            $action->show();
732
733            $runTime = microtime( true ) - $t;
734
735            $statAction = strtr( $actionName, '.', '_' );
736            $services->getStatsFactory()->getTiming( 'action_executeTiming_seconds' )
737                ->setLabel( 'action', $statAction )
738                ->copyToStatsdAt( 'action.' . $statAction . '.executeTiming' )
739                ->observe( 1000 * $runTime );
740
741            return;
742        }
743
744        // If we've not found out which action it is by now, it's unknown
745        $output->setStatusCode( 404 );
746        $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
747    }
748}