Merge "rest: Return a 400 for invalid render IDs"
[mediawiki.git] / includes / actions / ActionEntryPoint.php
blob74bfa6d9f3fbeb9bb52da6bc840ab99c58da8057
1 <?php
3 namespace MediaWiki\Actions;
5 use Action;
6 use Article;
7 use BadTitleError;
8 use ErrorPageError;
9 use HTMLFileCache;
10 use HttpError;
11 use MediaWiki\Context\RequestContext;
12 use MediaWiki\Logger\LoggerFactory;
13 use MediaWiki\MainConfigNames;
14 use MediaWiki\MediaWikiEntryPoint;
15 use MediaWiki\Output\OutputPage;
16 use MediaWiki\Permissions\PermissionStatus;
17 use MediaWiki\Profiler\ProfilingContext;
18 use MediaWiki\Request\DerivativeRequest;
19 use MediaWiki\Request\WebRequest;
20 use MediaWiki\SpecialPage\RedirectSpecialPage;
21 use MediaWiki\SpecialPage\SpecialPage;
22 use MediaWiki\Title\MalformedTitleException;
23 use MediaWiki\Title\Title;
24 use MediaWiki\User\User;
25 use MWExceptionRenderer;
26 use PermissionsError;
27 use Profiler;
28 use Throwable;
29 use UnexpectedValueException;
30 use ViewAction;
31 use WikiFilePage;
32 use Wikimedia\Rdbms\DBConnectionError;
34 /**
35 * The index.php entry point for web browser navigations, usually routed to
36 * an Action or SpecialPage subclass.
38 * @internal For use in index.php
39 * @ingroup entrypoint
41 class ActionEntryPoint extends MediaWikiEntryPoint {
43 /**
44 * Overwritten to narrow the return type to RequestContext
46 protected function getContext(): RequestContext {
47 /** @var RequestContext $context */
48 $context = parent::getContext();
50 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType see $context in the constructor
51 return $context;
54 protected function getOutput(): OutputPage {
55 return $this->getContext()->getOutput();
58 protected function getUser(): User {
59 return $this->getContext()->getUser();
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();
81 parent::handleTopLevelError( $e );
84 /**
85 * Determine and send the response headers and body for this web request
87 protected function execute() {
88 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
89 global $wgTitle;
91 // Get title from request parameters,
92 // is set on the fly by parseTitle the first time.
93 $title = $this->getTitle();
94 $wgTitle = $title;
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__ );
109 if ( $this->maybeDoHttpsRedirect() ) {
110 return;
113 $context = $this->getContext();
114 $output = $context->getOutput();
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 );
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();
139 return;
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.
155 $this->prepareForOutput();
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 ) );
166 * If the stars are suitably aligned, do an HTTP->HTTPS redirect
168 * Note: Do this after $wgTitle is setup, otherwise the hooks run from
169 * isRegistered() will do all sorts of weird stuff.
171 * @return bool True if the redirect was done. Handling of the request
172 * should be aborted. False if no redirect was done.
174 protected function maybeDoHttpsRedirect() {
175 if ( !$this->shouldDoHttpRedirect() ) {
176 return false;
179 $context = $this->getContext();
180 $request = $context->getRequest();
181 $oldUrl = $request->getFullRequestURL();
182 $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
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.
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" );
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();
204 return true;
207 protected function doPrepareForOutput() {
208 parent::doPrepareForOutput();
210 // If needed, push a deferred update to run jobs after the output is sent
211 $this->schedulePostSendJobs();
214 protected function schedulePostSendJobs() {
215 // Recursion guard for $wgRunJobsAsync
216 if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
217 return;
220 parent::schedulePostSendJobs();
224 * Parse the request to get the Title object
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
230 protected function parseTitle( $request ) {
231 $curid = $request->getInt( 'curid' );
232 $title = $request->getText( 'title' );
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 );
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() );
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 );
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()
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' );
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' ) )
294 $ret = SpecialPage::getTitleFor( 'Revisiondelete' );
295 } elseif (
296 $actionName === 'editchangetags' ||
297 ( $actionName === 'historysubmit' && $request->getBool( 'editchangetags' ) )
299 $ret = SpecialPage::getTitleFor( 'EditTags' );
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'
309 $ret = Title::newMainPage();
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 );
319 return $ret;
323 * Get the Title object that we'll be acting on, as specified in the WebRequest
324 * @return Title
326 public function getTitle() {
327 $context = $this->getContext();
329 if ( !$context->hasTitle() ) {
330 try {
331 $context->setTitle( $this->parseTitle( $context->getRequest() ) );
332 } catch ( MalformedTitleException $ex ) {
333 $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
336 return $context->getTitle();
340 * Returns the name of the action that will be executed.
342 * @note This is public for the benefit of extensions that implement
343 * the BeforeInitialize or MediaWikiPerformAction hooks.
345 * @return string Action
347 public function getAction(): string {
348 return $this->getContext()->getActionName();
352 * Performs the request.
353 * - bad titles
354 * - read restriction
355 * - local interwiki redirects
356 * - redirect loop
357 * - special pages
358 * - normal pages
360 * @throws PermissionsError|BadTitleError|HttpError
361 * @return void
363 protected function performRequest() {
364 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
365 global $wgTitle;
367 $context = $this->getContext();
369 $request = $context->getRequest();
370 $output = $context->getOutput();
372 if ( $request->getRawVal( 'printable' ) === 'yes' ) {
373 $output->setPrintable();
376 $user = $context->getUser();
377 $title = $context->getTitle();
378 $requestTitle = $title;
380 $userOptionsLookup = $this->getServiceContainer()->getUserOptionsLookup();
381 if ( $userOptionsLookup->getBoolOption( $user, 'forcesafemode' ) ) {
382 $request->setVal( 'safemode', '1' );
385 $this->getHookRunner()->onBeforeInitialize( $title, null, $output, $user, $request, $this );
387 // Invalid titles. T23776: The interwikis must redirect even if the page name is empty.
388 if ( $title === null || ( $title->getDBkey() == '' && !$title->isExternal() )
389 || $title->isSpecial( 'Badtitle' )
391 $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
392 try {
393 $this->parseTitle( $request );
394 } catch ( MalformedTitleException $ex ) {
395 throw new BadTitleError( $ex );
397 throw new BadTitleError();
400 // Check user's permissions to read this page.
401 // We have to check here to catch special pages etc.
402 // We will check again in Article::view().
403 $permissionStatus = PermissionStatus::newEmpty();
404 if ( !$context->getAuthority()->authorizeRead( 'read', $title, $permissionStatus ) ) {
405 // T34276: allowing the skin to generate output with $wgTitle or
406 // $context->title set to the input title would allow anonymous users to
407 // determine whether a page exists, potentially leaking private data. In fact, the
408 // curid and oldid request parameters would allow page titles to be enumerated even
409 // when they are not guessable. So we reset the title to Special:Badtitle before the
410 // permissions error is displayed.
412 // The skin mostly uses $context->getTitle() these days, but some extensions
413 // still use $wgTitle.
414 $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
415 $context->setTitle( $badTitle );
416 $wgTitle = $badTitle;
418 throw new PermissionsError( 'read', $permissionStatus );
421 // Interwiki redirects
422 if ( $title->isExternal() ) {
423 $rdfrom = $request->getVal( 'rdfrom' );
424 if ( $rdfrom ) {
425 $url = $title->getFullURL( [ 'rdfrom' => $rdfrom ] );
426 } else {
427 $query = $request->getQueryValues();
428 unset( $query['title'] );
429 $url = $title->getFullURL( $query );
431 // Check for a redirect loop
432 if ( $url !== $request->getFullRequestURL() && $title->isLocal() ) {
433 // 301 so google et al report the target as the actual url.
434 $output->redirect( $url, 301 );
435 } else {
436 $context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
437 try {
438 $this->parseTitle( $request );
439 } catch ( MalformedTitleException $ex ) {
440 throw new BadTitleError( $ex );
442 throw new BadTitleError();
444 // Handle any other redirects.
445 // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
446 } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
447 // Prevent information leak via Special:MyPage et al (T109724)
448 $spFactory = $this->getServiceContainer()->getSpecialPageFactory();
449 if ( $title->isSpecialPage() ) {
450 $specialPage = $spFactory->getPage( $title->getDBkey() );
451 if ( $specialPage instanceof RedirectSpecialPage ) {
452 $specialPage->setContext( $context );
453 if ( $this->getConfig( MainConfigNames::HideIdentifiableRedirects )
454 && $specialPage->personallyIdentifiableTarget()
456 [ , $subpage ] = $spFactory->resolveAlias( $title->getDBkey() );
457 $target = $specialPage->getRedirect( $subpage );
458 // Target can also be true. We let that case fall through to normal processing.
459 if ( $target instanceof Title ) {
460 if ( $target->isExternal() ) {
461 // Handle interwiki redirects
462 $target = SpecialPage::getTitleFor(
463 'GoToInterwiki',
464 'force/' . $target->getPrefixedDBkey()
468 $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
469 $derivateRequest = new DerivativeRequest( $request, $query );
470 $derivateRequest->setRequestURL( $request->getRequestURL() );
471 $context->setRequest( $derivateRequest );
472 // Do not varnish cache these. May vary even for anons
473 $output->lowerCdnMaxage( 0 );
474 // NOTE: This also clears any action cache.
475 // Action should not have been computed yet, but if it was,
476 // we reset it because special pages only support "view".
477 $context->setTitle( $target );
478 $wgTitle = $target;
479 $title = $target;
480 $output->addJsConfigVars( [
481 'wgInternalRedirectTargetUrl' => $target->getLinkURL( $query ),
482 ] );
483 $output->addModules( 'mediawiki.action.view.redirect' );
485 // If the title is invalid, redirect but show the correct bad title error - T297407
486 if ( !$target->isValid() ) {
487 try {
488 $this->getServiceContainer()->getTitleParser()
489 ->parseTitle( $target->getPrefixedText() );
490 } catch ( MalformedTitleException $ex ) {
491 throw new BadTitleError( $ex );
493 throw new BadTitleError();
500 // Special pages ($title may have changed since if statement above)
501 if ( $title->isSpecialPage() ) {
502 // Actions that need to be made when we have a special pages
503 $spFactory->executePath( $title, $context );
504 } else {
505 // ...otherwise treat it as an article view. The article
506 // may still be a wikipage redirect to another article or URL.
507 $article = $this->initializeArticle();
508 if ( is_object( $article ) ) {
509 $this->performAction( $article, $requestTitle );
510 } elseif ( is_string( $article ) ) {
511 $output->redirect( $article );
512 } else {
513 throw new UnexpectedValueException( "Shouldn't happen: MediaWiki::initializeArticle()"
514 . " returned neither an object nor a URL" );
517 $output->considerCacheSettingsFinal();
522 * Handle redirects for uncanonical title requests.
524 * Handles:
525 * - Redirect loops.
526 * - No title in URL.
527 * - $wgUsePathInfo URLs.
528 * - URLs with a variant.
529 * - Other non-standard URLs (as long as they have no extra query parameters).
531 * Behaviour:
532 * - Normalise title values:
533 * /wiki/Foo%20Bar -> /wiki/Foo_Bar
534 * - Normalise empty title:
535 * /wiki/ -> /wiki/Main
536 * /w/index.php?title= -> /wiki/Main
537 * - Don't redirect anything with query parameters other than 'title' or 'action=view'.
539 * @param Title $title
540 * @return bool True if a redirect was set.
541 * @throws HttpError
543 protected function tryNormaliseRedirect( Title $title ): bool {
544 $request = $this->getRequest();
545 $output = $this->getOutput();
547 if ( ( $request->getRawVal( 'action' ) ?? 'view' ) !== 'view'
548 || $request->wasPosted()
549 || ( $request->getCheck( 'title' )
550 && $title->getPrefixedDBkey() == $request->getText( 'title' ) )
551 || count( $request->getValueNames( [ 'action', 'title' ] ) )
552 || !$this->getHookRunner()->onTestCanonicalRedirect( $request, $title, $output )
554 return false;
557 if ( $this->getConfig( MainConfigNames::MainPageIsDomainRoot ) && $request->getRequestURL() === '/' ) {
558 return false;
561 $services = $this->getServiceContainer();
563 if ( $title->isSpecialPage() ) {
564 [ $name, $subpage ] = $services->getSpecialPageFactory()
565 ->resolveAlias( $title->getDBkey() );
567 if ( $name ) {
568 $title = SpecialPage::getTitleFor( $name, $subpage );
571 // Redirect to canonical url, make it a 301 to allow caching
572 $targetUrl = (string)$services->getUrlUtils()->expand( $title->getFullURL(), PROTO_CURRENT );
573 if ( $targetUrl == $request->getFullRequestURL() ) {
574 $message = "Redirect loop detected!\n\n" .
575 "This means the wiki got confused about what page was " .
576 "requested; this sometimes happens when moving a wiki " .
577 "to a new server or changing the server configuration.\n\n";
579 if ( $this->getConfig( MainConfigNames::UsePathInfo ) ) {
580 $message .= "The wiki is trying to interpret the page " .
581 "title from the URL path portion (PATH_INFO), which " .
582 "sometimes fails depending on the web server. Try " .
583 "setting \"\$wgUsePathInfo = false;\" in your " .
584 "LocalSettings.php, or check that \$wgArticlePath " .
585 "is correct.";
586 } else {
587 $message .= "Your web server was detected as possibly not " .
588 "supporting URL path components (PATH_INFO) correctly; " .
589 "check your LocalSettings.php for a customized " .
590 "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
591 "to true.";
593 throw new HttpError( 500, $message );
595 $output->setCdnMaxage( 1200 );
596 $output->redirect( $targetUrl, '301' );
597 return true;
601 * Initialize the main Article object for "standard" actions (view, etc)
602 * Create an Article object for the page, following redirects if needed.
604 * @return Article|string An Article, or a string to redirect to another URL
606 protected function initializeArticle() {
607 $context = $this->getContext();
609 $title = $context->getTitle();
610 $services = $this->getServiceContainer();
611 if ( $context->canUseWikiPage() ) {
612 // Optimization: Reuse the WikiPage instance from context, to avoid
613 // repeat fetching or computation of data already loaded.
614 $page = $context->getWikiPage();
615 } else {
616 // This case should not happen, but just in case.
617 // @TODO: remove this or use an exception
618 $page = $services->getWikiPageFactory()->newFromTitle( $title );
619 $context->setWikiPage( $page );
620 wfWarn( "RequestContext::canUseWikiPage() returned false" );
623 // Make GUI wrapper for the WikiPage
624 $article = Article::newFromWikiPage( $page, $context );
626 // Skip some unnecessary code if the content model doesn't support redirects
627 // Use the page content model rather than invoking Title::getContentModel()
628 // to avoid querying page data twice (T206498)
629 if ( !$page->getContentHandler()->supportsRedirects() ) {
630 return $article;
633 $request = $context->getRequest();
635 // Namespace might change when using redirects
636 // Check for redirects ...
637 $action = $request->getRawVal( 'action' ) ?? 'view';
638 $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null;
639 if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
640 && !$request->getCheck( 'oldid' ) // ... and are not old revisions
641 && !$request->getCheck( 'diff' ) // ... and not when showing diff
642 && $request->getRawVal( 'redirect' ) !== 'no' // ... unless explicitly told not to
643 // ... and the article is not a non-redirect image page with associated file
644 && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
646 // Give extensions a change to ignore/handle redirects as needed
647 $ignoreRedirect = $target = false;
649 $this->getHookRunner()->onInitializeArticleMaybeRedirect( $title, $request,
650 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
651 $ignoreRedirect, $target, $article );
652 $page = $article->getPage(); // reflect any hook changes
654 // Follow redirects only for... redirects.
655 // If $target is set, then a hook wanted to redirect.
656 if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
657 // Is the target already set by an extension?
658 $target = $target ?: $page->followRedirect();
659 if ( is_string( $target ) && !$this->getConfig( MainConfigNames::DisableHardRedirects ) ) {
660 // we'll need to redirect
661 return $target;
663 if ( is_object( $target ) ) {
664 // Rewrite environment to redirected article
665 $rpage = $services->getWikiPageFactory()->newFromTitle( $target );
666 $rpage->loadPageData();
667 if ( $rpage->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
668 $rarticle = Article::newFromWikiPage( $rpage, $context );
669 $rarticle->setRedirectedFrom( $title );
671 $article = $rarticle;
672 // NOTE: This also clears any action cache
673 $context->setTitle( $target );
674 $context->setWikiPage( $article->getPage() );
680 return $article;
684 * Perform one of the "standard" actions
686 * @param Article $article
687 * @param Title $requestTitle The original title, before any redirects were applied
689 protected function performAction( Article $article, Title $requestTitle ) {
690 $request = $this->getRequest();
691 $output = $this->getOutput();
692 $title = $this->getTitle();
693 $user = $this->getUser();
695 if ( !$this->getHookRunner()->onMediaWikiPerformAction(
696 $output, $article, $title, $user, $request, $this )
698 return;
701 $t = microtime( true );
702 $actionName = $this->getAction();
703 $services = $this->getServiceContainer();
705 $action = $services->getActionFactory()->getAction( $actionName, $article, $this->getContext() );
706 if ( $action instanceof Action ) {
707 ProfilingContext::singleton()->init( MW_ENTRY_POINT, $actionName );
709 // Check read permissions
710 if ( $action->needsReadRights() && !$user->isAllowed( 'read' ) ) {
711 throw new PermissionsError( 'read' );
714 // Narrow DB query expectations for this HTTP request
715 if ( $request->wasPosted() && !$action->doesWrites() ) {
716 $trxProfiler = Profiler::instance()->getTransactionProfiler();
717 $trxLimits = $this->getConfig( MainConfigNames::TrxProfilerLimits );
718 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
721 // Let CDN cache things if we can purge them.
722 // Also unconditionally cache page views.
723 if ( $this->getConfig( MainConfigNames::UseCdn ) ) {
724 $htmlCacheUpdater = $services->getHtmlCacheUpdater();
725 if ( $request->matchURLForCDN( $htmlCacheUpdater->getUrls( $requestTitle ) ) ) {
726 $output->setCdnMaxage( $this->getConfig( MainConfigNames::CdnMaxAge ) );
727 } elseif ( $action instanceof ViewAction ) {
728 $output->setCdnMaxage( 3600 );
732 $action->show();
734 $runTime = microtime( true ) - $t;
736 $statAction = strtr( $actionName, '.', '_' );
737 $services->getStatsFactory()->getTiming( 'action_executeTiming_seconds' )
738 ->setLabel( 'action', $statAction )
739 ->copyToStatsdAt( 'action.' . $statAction . '.executeTiming' )
740 ->observe( 1000 * $runTime );
742 return;
745 // If we've not found out which action it is by now, it's unknown
746 $output->setStatusCode( 404 );
747 $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );