Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.83% covered (warning)
65.83%
210 / 319
37.50% covered (danger)
37.50%
6 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActionEntryPoint
65.83% covered (warning)
65.83%
210 / 319
37.50% covered (danger)
37.50%
6 / 16
827.11
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.45% covered (success)
95.45%
42 / 44
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
 getHookRunner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 performRequest
70.00% covered (warning)
70.00%
63 / 90
0.00% covered (danger)
0.00%
0 / 1
51.71
 tryNormaliseRedirect
46.15% covered (danger)
46.15%
18 / 39
0.00% covered (danger)
0.00%
0 / 1
39.38
 initializeArticle
86.49% covered (warning)
86.49%
32 / 37
0.00% covered (danger)
0.00%
0 / 1
23.19
 performAction
62.50% covered (warning)
62.50%
20 / 32
0.00% covered (danger)
0.00%
0 / 1
15.27
1<?php
2
3namespace MediaWiki\Actions;
4
5use MediaWiki\Cache\HTMLFileCache;
6use MediaWiki\Context\RequestContext;
7use MediaWiki\Exception\BadTitleError;
8use MediaWiki\Exception\ErrorPageError;
9use MediaWiki\Exception\HttpError;
10use MediaWiki\Exception\MWExceptionRenderer;
11use MediaWiki\Exception\PermissionsError;
12use MediaWiki\HookContainer\HookRunner;
13use MediaWiki\Logger\LoggerFactory;
14use MediaWiki\MainConfigNames;
15use MediaWiki\MediaWikiEntryPoint;
16use MediaWiki\Output\OutputPage;
17use MediaWiki\Page\Article;
18use MediaWiki\Page\WikiFilePage;
19use MediaWiki\Permissions\PermissionStatus;
20use MediaWiki\Profiler\Profiler;
21use MediaWiki\Profiler\ProfilingContext;
22use MediaWiki\Request\DerivativeRequest;
23use MediaWiki\Request\FauxRequest;
24use MediaWiki\Request\WebRequest;
25use MediaWiki\SpecialPage\RedirectSpecialPage;
26use MediaWiki\SpecialPage\SpecialPage;
27use MediaWiki\Title\MalformedTitleException;
28use MediaWiki\Title\Title;
29use MediaWiki\User\User;
30use Throwable;
31use UnexpectedValueException;
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     */
46    protected function getContext(): RequestContext {
47        /** @var RequestContext $context */
48        $context = parent::getContext();
49
50        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType see $context in the constructor
51        return $context;
52    }
53
54    protected function getOutput(): OutputPage {
55        return $this->getContext()->getOutput();
56    }
57
58    protected function getUser(): User {
59        return $this->getContext()->getUser();
60    }
61
62    protected function handleTopLevelError( Throwable $e ) {
63        $context = $this->getContext();
64        $action = $context->getRequest()->getRawVal( 'action' ) ?? 'view';
65        if (
66            $e instanceof DBConnectionError &&
67            $context->hasTitle() &&
68            $context->getTitle()->canExist() &&
69            in_array( $action, [ 'view', 'history' ], true ) &&
70            HTMLFileCache::useFileCache( $context, HTMLFileCache::MODE_OUTAGE )
71        ) {
72            // Try to use any (even stale) file during outages...
73            $cache = new HTMLFileCache( $context->getTitle(), $action );
74            if ( $cache->isCached() ) {
75                $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE );
76                $this->print( MWExceptionRenderer::getHTML( $e ) );
77                $this->exit();
78            }
79        }
80
81        parent::handleTopLevelError( $e );
82    }
83
84    /**
85     * Determine and send the response headers and body for this web request
86     */
87    protected function execute() {
88        // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
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->setStatsFactory( $this->getStatsFactory() );
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::newFromPageIdentity( $revRecord->getPage() );
274                }
275            }
276        }
277
278        if ( $ret === null && $request->getCheck( 'search' ) ) {
279            // Compatibility with old search URLs which didn't use Special:Search
280            // Just check for presence here, so blank requests still
281            // show the search page when using ugly URLs (T10054).
282            $ret = SpecialPage::getTitleFor( 'Search' );
283        }
284
285        if ( $ret === null || !$ret->isSpecialPage() ) {
286            // Compatibility with old URLs for Special:RevisionDelete/Special:EditTags (T323338)
287            $actionName = $request->getRawVal( 'action' );
288            if (
289                $actionName === 'revisiondelete' ||
290                ( $actionName === 'historysubmit' && $request->getBool( 'revisiondelete' ) )
291            ) {
292                $ret = SpecialPage::getTitleFor( 'Revisiondelete' );
293            } elseif (
294                $actionName === 'editchangetags' ||
295                ( $actionName === 'historysubmit' && $request->getBool( 'editchangetags' ) )
296            ) {
297                $ret = SpecialPage::getTitleFor( 'EditTags' );
298            }
299        }
300
301        // Use the main page as default title if nothing else has been provided
302        if ( $ret === null
303            && strval( $title ) === ''
304            && !$request->getCheck( 'curid' )
305            && $request->getRawVal( 'action' ) !== 'delete'
306        ) {
307            $ret = Title::newMainPage();
308        }
309
310        if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) {
311            // If we get here, we definitely don't have a valid title; throw an exception.
312            // Try to get detailed invalid title exception first, fall back to MalformedTitleException.
313            Title::newFromTextThrow( $title );
314            throw new MalformedTitleException( 'badtitletext', $title );
315        }
316
317        return $ret;
318    }
319
320    /**
321     * Get the Title object that we'll be acting on, as specified in the WebRequest
322     * @return Title
323     */
324    public function getTitle() {
325        $context = $this->getContext();
326
327        if ( !$context->hasTitle() ) {
328            try {
329                $context->setTitle( $this->parseTitle( $context->getRequest() ) );
330            } catch ( MalformedTitleException ) {
331                $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
332            }
333        }
334        return $context->getTitle();
335    }
336
337    /**
338     * Returns the name of the action that will be executed.
339     *
340     * @note This is public for the benefit of extensions that implement
341     * the BeforeInitialize or MediaWikiPerformAction hooks.
342     *
343     * @return string Action
344     */
345    public function getAction(): string {
346        return $this->getContext()->getActionName();
347    }
348
349    private function getHookRunner(): HookRunner {
350        return new HookRunner( $this->mediaWikiServices->getHookContainer() );
351    }
352
353    /**
354     * Performs the request.
355     * - bad titles
356     * - read restriction
357     * - local interwiki redirects
358     * - redirect loop
359     * - special pages
360     * - normal pages
361     *
362     * @throws PermissionsError|BadTitleError|HttpError
363     * @return void
364     */
365    protected function performRequest() {
366        // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
367        global $wgTitle;
368
369        $context = $this->getContext();
370
371        $request = $context->getRequest();
372        $output = $context->getOutput();
373
374        if ( $request->getRawVal( 'printable' ) === 'yes' ) {
375            $output->setPrintable();
376        }
377
378        $user = $context->getUser();
379        $title = $context->getTitle();
380        $requestTitle = $title;
381
382        $userOptionsLookup = $this->getServiceContainer()->getUserOptionsLookup();
383        if ( $userOptionsLookup->getBoolOption( $user, 'forcesafemode' ) ) {
384            $request->setVal( 'safemode', '1' );
385        }
386
387        $this->getHookRunner()->onBeforeInitialize( $title, null, $output, $user, $request, $this );
388
389        // Invalid titles. T23776: The interwikis must redirect even if the page name is empty.
390        if ( $title === null || ( $title->getDBkey() == '' && !$title->isExternal() )
391            || $title->isSpecial( 'Badtitle' )
392        ) {
393            $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
394            try {
395                $this->parseTitle( $request );
396            } catch ( MalformedTitleException $ex ) {
397                throw new BadTitleError( $ex );
398            }
399            throw new BadTitleError();
400        }
401
402        // Check user's permissions to read this page.
403        // We have to check here to catch special pages etc.
404        // We will check again in Article::view().
405        $permissionStatus = PermissionStatus::newEmpty();
406        if ( !$context->getAuthority()->authorizeRead( 'read', $title, $permissionStatus ) ) {
407            // T34276: allowing the skin to generate output with $wgTitle or
408            // $context->title set to the input title would allow anonymous users to
409            // determine whether a page exists, potentially leaking private data. In fact, the
410            // curid and oldid request  parameters would allow page titles to be enumerated even
411            // when they are not guessable. So we reset the title to Special:Badtitle before the
412            // permissions error is displayed.
413
414            // The skin mostly uses $context->getTitle() these days, but some extensions
415            // still use $wgTitle.
416            $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
417            $context->setTitle( $badTitle );
418            $wgTitle = $badTitle;
419
420            throw new PermissionsError( 'read', $permissionStatus );
421        }
422
423        // Interwiki redirects
424        if ( $title->isExternal() ) {
425            $rdfrom = $request->getVal( 'rdfrom' );
426            if ( $rdfrom ) {
427                $url = $title->getFullURLForRedirect( [ 'rdfrom' => $rdfrom ] );
428            } else {
429                $query = $request->getQueryValues();
430                unset( $query['title'] );
431                $url = $title->getFullURLForRedirect( $query );
432            }
433            // Check for a redirect loop
434            if ( $url !== $request->getFullRequestURL() ) {
435                // 301 so google et al report the target as the actual url.
436                $output->redirect( $url, 301 );
437            } else {
438                $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
439                try {
440                    $this->parseTitle( $request );
441                } catch ( MalformedTitleException $ex ) {
442                    throw new BadTitleError( $ex );
443                }
444                throw new BadTitleError();
445            }
446            // Handle any other redirects.
447            // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
448        } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
449            // Prevent information leak via Special:MyPage et al (T109724)
450            $spFactory = $this->getServiceContainer()->getSpecialPageFactory();
451            if ( $title->isSpecialPage() ) {
452                $specialPage = $spFactory->getPage( $title->getDBkey() );
453                if ( $specialPage instanceof RedirectSpecialPage ) {
454                    $specialPage->setContext( $context );
455                    if ( $this->getConfig( MainConfigNames::HideIdentifiableRedirects )
456                        && $specialPage->personallyIdentifiableTarget()
457                    ) {
458                        [ , $subpage ] = $spFactory->resolveAlias( $title->getDBkey() );
459                        $target = $specialPage->getRedirect( $subpage );
460                        // Target can also be true. We let that case fall through to normal processing.
461                        if ( $target instanceof Title ) {
462                            if ( $target->isExternal() ) {
463                                // Handle interwiki redirects
464                                $target = SpecialPage::getTitleFor(
465                                    'GoToInterwiki',
466                                    'force/' . $target->getPrefixedDBkey()
467                                );
468                            }
469
470                            $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
471                            $derivateRequest = new DerivativeRequest( $request, $query );
472                            $derivateRequest->setRequestURL( $request->getRequestURL() );
473                            $context->setRequest( $derivateRequest );
474                            // Do not varnish cache these. May vary even for anons
475                            $output->lowerCdnMaxage( 0 );
476                            // NOTE: This also clears any action cache.
477                            // Action should not have been computed yet, but if it was,
478                            // we reset it because special pages only support "view".
479                            $context->setTitle( $target );
480                            $wgTitle = $target;
481                            $title = $target;
482                            $output->addJsConfigVars( [
483                                'wgInternalRedirectTargetUrl' => $target->getLinkURL( $query ),
484                            ] );
485                            $output->addModules( 'mediawiki.action.view.redirect' );
486
487                            // If the title is invalid, redirect but show the correct bad title error - T297407
488                            if ( !$target->isValid() ) {
489                                try {
490                                    $this->getServiceContainer()->getTitleParser()
491                                        ->parseTitle( $target->getPrefixedText() );
492                                } catch ( MalformedTitleException $ex ) {
493                                    throw new BadTitleError( $ex );
494                                }
495                                throw new BadTitleError();
496                            }
497                        }
498                    }
499                }
500            }
501
502            // Special pages ($title may have changed since if statement above)
503            if ( $title->isSpecialPage() ) {
504                // Actions that need to be made when we have a special pages
505                $spFactory->executePath( $title, $context );
506            } else {
507                // ...otherwise treat it as an article view. The article
508                // may still be a wikipage redirect to another article or URL.
509                $article = $this->initializeArticle();
510                if ( is_object( $article ) ) {
511                    $this->performAction( $article, $requestTitle );
512                } elseif ( is_string( $article ) ) {
513                    $output->redirect( $article );
514                } else {
515                    throw new UnexpectedValueException( "Shouldn't happen: MediaWiki::initializeArticle()"
516                        . " returned neither an object nor a URL" );
517                }
518            }
519
520            $contextRequest = $context->getRequest();
521            if ( $contextRequest instanceof FauxRequest ) {
522                $fauxResponse = $contextRequest->response();
523                if ( $fauxResponse->getStatusCode() ) {
524                    $request->response()->statusHeader( $fauxResponse->getStatusCode() );
525                }
526
527                foreach ( $fauxResponse->getHeaders() as $key => $value ) {
528                    $request->response()->header( "$key$value" );
529                }
530            }
531
532            $output->considerCacheSettingsFinal();
533        }
534    }
535
536    /**
537     * Handle redirects for uncanonical title requests.
538     *
539     * Handles:
540     * - Redirect loops.
541     * - No title in URL.
542     * - $wgUsePathInfo URLs.
543     * - URLs with a variant.
544     * - Other non-standard URLs (as long as they have no extra query parameters).
545     *
546     * Behaviour:
547     * - Normalise title values:
548     *   /wiki/Foo%20Bar -> /wiki/Foo_Bar
549     * - Normalise empty title:
550     *   /wiki/ -> /wiki/Main
551     *   /w/index.php?title= -> /wiki/Main
552     * - Don't redirect anything with query parameters other than 'title' or 'action=view'.
553     *
554     * @param Title $title
555     * @return bool True if a redirect was set.
556     * @throws HttpError
557     */
558    protected function tryNormaliseRedirect( Title $title ): bool {
559        $request = $this->getRequest();
560        $output = $this->getOutput();
561
562        if ( ( $request->getRawVal( 'action' ) ?? 'view' ) !== 'view'
563            || $request->wasPosted()
564            || ( $request->getCheck( 'title' )
565                && $title->getPrefixedDBkey() == $request->getText( 'title' ) )
566            || count( array_diff( array_keys( $request->getQueryValues() ), [ 'action', 'title' ] ) )
567            || !$this->getHookRunner()->onTestCanonicalRedirect( $request, $title, $output )
568        ) {
569            return false;
570        }
571
572        if ( $this->getConfig( MainConfigNames::MainPageIsDomainRoot ) && $request->getRequestURL() === '/' ) {
573            return false;
574        }
575
576        $services = $this->getServiceContainer();
577
578        if ( $title->isSpecialPage() ) {
579            [ $name, $subpage ] = $services->getSpecialPageFactory()
580                ->resolveAlias( $title->getDBkey() );
581
582            if ( $name ) {
583                $title = SpecialPage::getTitleFor( $name, $subpage );
584            }
585        }
586        // Redirect to canonical url, make it a 301 to allow caching
587        $targetUrl = (string)$services->getUrlUtils()->expand( $title->getFullURL(), PROTO_CURRENT );
588        if ( $targetUrl == $request->getFullRequestURL() ) {
589            $message = "Redirect loop detected!\n\n" .
590                "This means the wiki got confused about what page was " .
591                "requested; this sometimes happens when moving a wiki " .
592                "to a new server or changing the server configuration.\n\n";
593
594            if ( $this->getConfig( MainConfigNames::UsePathInfo ) ) {
595                $message .= "The wiki is trying to interpret the page " .
596                    "title from the URL path portion (PATH_INFO), which " .
597                    "sometimes fails depending on the web server. Try " .
598                    "setting \"\$wgUsePathInfo = false;\" in your " .
599                    "LocalSettings.php, or check that \$wgArticlePath " .
600                    "is correct.";
601            } else {
602                $message .= "Your web server was detected as possibly not " .
603                    "supporting URL path components (PATH_INFO) correctly; " .
604                    "check your LocalSettings.php for a customized " .
605                    "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
606                    "to true.";
607            }
608            throw new HttpError( 500, $message );
609        }
610        $output->setCdnMaxage( 1200 );
611        $output->redirect( $targetUrl, '301' );
612        return true;
613    }
614
615    /**
616     * Initialize the main Article object for "standard" actions (view, etc)
617     * Create an Article object for the page, following redirects if needed.
618     *
619     * @return Article|string An Article, or a string to redirect to another URL
620     */
621    protected function initializeArticle() {
622        $context = $this->getContext();
623
624        $title = $context->getTitle();
625        $services = $this->getServiceContainer();
626        if ( $context->canUseWikiPage() ) {
627            // Optimization: Reuse the WikiPage instance from context, to avoid
628            // repeat fetching or computation of data already loaded.
629            $page = $context->getWikiPage();
630        } else {
631            // This case should not happen, but just in case.
632            // @TODO: remove this or use an exception
633            $page = $services->getWikiPageFactory()->newFromTitle( $title );
634            $context->setWikiPage( $page );
635            wfWarn( "RequestContext::canUseWikiPage() returned false" );
636        }
637
638        // Make GUI wrapper for the WikiPage
639        $article = Article::newFromWikiPage( $page, $context );
640
641        // Skip some unnecessary code if the content model doesn't support redirects
642        // Use the page content model rather than invoking Title::getContentModel()
643        // to avoid querying page data twice (T206498)
644        if ( !$page->getContentHandler()->supportsRedirects() ) {
645            return $article;
646        }
647
648        $request = $context->getRequest();
649
650        // Namespace might change when using redirects
651        // Check for redirects ...
652        $action = $request->getRawVal( 'action' ) ?? 'view';
653        $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null;
654        if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
655            && !$request->getCheck( 'oldid' ) // ... and are not old revisions
656            && !$request->getCheck( 'diff' ) // ... and not when showing diff
657            && $request->getRawVal( 'redirect' ) !== 'no' // ... unless explicitly told not to
658            // ... and the article is not a non-redirect image page with associated file
659            && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
660        ) {
661            // Give extensions a change to ignore/handle redirects as needed
662            $ignoreRedirect = $target = false;
663
664            $this->getHookRunner()->onInitializeArticleMaybeRedirect( $title, $request,
665                // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
666                $ignoreRedirect, $target, $article );
667            $page = $article->getPage(); // reflect any hook changes
668
669            // Follow redirects only for... redirects.
670            // If $target is set, then a hook wanted to redirect.
671            if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
672                // Is the target already set by an extension?
673                $target = $target ?: $page->followRedirect();
674                if ( is_string( $target ) && !$this->getConfig( MainConfigNames::DisableHardRedirects ) ) {
675                    // we'll need to redirect
676                    return $target;
677                }
678                if ( is_object( $target ) ) {
679                    // Rewrite environment to redirected article
680                    $rpage = $services->getWikiPageFactory()->newFromTitle( $target );
681                    $rpage->loadPageData();
682                    if ( $target->isKnown() || ( is_object( $file ) && !$file->isLocal() ) ) {
683                        $rarticle = Article::newFromWikiPage( $rpage, $context );
684                        $rarticle->setRedirectedFrom( $title );
685
686                        $article = $rarticle;
687                        // NOTE: This also clears any action cache
688                        $context->setTitle( $target );
689                        $context->setWikiPage( $article->getPage() );
690                    }
691                }
692            }
693        }
694
695        return $article;
696    }
697
698    /**
699     * Perform one of the "standard" actions
700     *
701     * @param Article $article
702     * @param Title $requestTitle The original title, before any redirects were applied
703     */
704    protected function performAction( Article $article, Title $requestTitle ) {
705        $request = $this->getRequest();
706        $output = $this->getOutput();
707        $title = $this->getTitle();
708        $user = $this->getUser();
709
710        if ( !$this->getHookRunner()->onMediaWikiPerformAction(
711            $output, $article, $title, $user, $request, $this )
712        ) {
713            return;
714        }
715
716        $actionName = $this->getAction();
717        $services = $this->getServiceContainer();
718        // To avoid garbage in the data we do not submit this timer when:
719        // * nosuchaction: The action is not defined in the code, e.g. action=blabla.
720        // * PermissionsError: The action handler Action::show is not even called,
721        //   and these fast errors should not influence the latency graph.
722        $timer = $services->getStatsFactory()->getTiming( 'action_executeTiming_seconds' )
723            ->setLabel( 'action', strtr( $actionName, '.', '_' ) )
724            ->start();
725
726        $action = $services->getActionFactory()->getAction( $actionName, $article, $this->getContext() );
727        if ( $action instanceof Action ) {
728            // Check read permissions
729            if ( $action->needsReadRights() && !$user->isAllowed( 'read' ) ) {
730                throw new PermissionsError( 'read' );
731            }
732
733            ProfilingContext::singleton()->init( MW_ENTRY_POINT, $actionName );
734
735            // Narrow DB query expectations for this HTTP request
736            if ( $request->wasPosted() && !$action->doesWrites() ) {
737                $trxProfiler = Profiler::instance()->getTransactionProfiler();
738                $trxLimits = $this->getConfig( MainConfigNames::TrxProfilerLimits );
739                $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
740            }
741
742            // Let CDN cache things if we can purge them.
743            // Also unconditionally cache page views.
744            if ( $this->getConfig( MainConfigNames::UseCdn ) ) {
745                $htmlCacheUpdater = $services->getHTMLCacheUpdater();
746                if ( $request->matchURLForCDN( $htmlCacheUpdater->getUrls( $requestTitle ) ) ) {
747                    $output->setCdnMaxage( $this->getConfig( MainConfigNames::CdnMaxAge ) );
748                } elseif ( $action instanceof ViewAction ) {
749                    $output->setCdnMaxage( 3600 );
750                }
751            }
752
753            $action->show();
754
755            $timer->stop();
756
757            return;
758        }
759
760        // If we've not found out which action it is by now, it's unknown
761        $output->setStatusCode( 404 );
762        $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
763    }
764}