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