MediaWiki REL1_34
MediaWiki.php
Go to the documentation of this file.
1<?php
24use Psr\Log\LoggerInterface;
29use Liuggio\StatsdClient\Sender\SocketSender;
30
34class MediaWiki {
38 private $context;
39
43 private $config;
44
48 private $action;
49
53 public function __construct( IContextSource $context = null ) {
54 if ( !$context ) {
55 $context = RequestContext::getMain();
56 }
57
58 $this->context = $context;
59 $this->config = $context->getConfig();
60 }
61
68 private function parseTitle() {
69 $request = $this->context->getRequest();
70 $curid = $request->getInt( 'curid' );
71 $title = $request->getVal( 'title' );
72 $action = $request->getVal( 'action' );
73
74 if ( $request->getCheck( 'search' ) ) {
75 // Compatibility with old search URLs which didn't use Special:Search
76 // Just check for presence here, so blank requests still
77 // show the search page when using ugly URLs (T10054).
78 $ret = SpecialPage::getTitleFor( 'Search' );
79 } elseif ( $curid ) {
80 // URLs like this are generated by RC, because rc_title isn't always accurate
81 $ret = Title::newFromID( $curid );
82 } else {
83 $ret = Title::newFromURL( $title );
84 // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
85 // in wikitext links to tell Parser to make a direct file link
86 if ( !is_null( $ret ) && $ret->getNamespace() == NS_MEDIA ) {
87 $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() );
88 }
89 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
90 // Check variant links so that interwiki links don't have to worry
91 // about the possible different language variants
92 if (
93 $contLang->hasVariants() && !is_null( $ret ) && $ret->getArticleID() == 0
94 ) {
95 $contLang->findVariantLink( $title, $ret );
96 }
97 }
98
99 // If title is not provided, always allow oldid and diff to set the title.
100 // If title is provided, allow oldid and diff to override the title, unless
101 // we are talking about a special page which might use these parameters for
102 // other purposes.
103 if ( $ret === null || !$ret->isSpecialPage() ) {
104 // We can have urls with just ?diff=,?oldid= or even just ?diff=
105 $oldid = $request->getInt( 'oldid' );
106 $oldid = $oldid ?: $request->getInt( 'diff' );
107 // Allow oldid to override a changed or missing title
108 if ( $oldid ) {
109 $rev = Revision::newFromId( $oldid );
110 $ret = $rev ? $rev->getTitle() : $ret;
111 }
112 }
113
114 // Use the main page as default title if nothing else has been provided
115 if ( $ret === null
116 && strval( $title ) === ''
117 && !$request->getCheck( 'curid' )
118 && $action !== 'delete'
119 ) {
120 $ret = Title::newMainPage();
121 }
122
123 if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) {
124 // If we get here, we definitely don't have a valid title; throw an exception.
125 // Try to get detailed invalid title exception first, fall back to MalformedTitleException.
126 Title::newFromTextThrow( $title );
127 throw new MalformedTitleException( 'badtitletext', $title );
128 }
129
130 return $ret;
131 }
132
137 public function getTitle() {
138 if ( !$this->context->hasTitle() ) {
139 try {
140 $this->context->setTitle( $this->parseTitle() );
141 } catch ( MalformedTitleException $ex ) {
142 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
143 }
144 }
145 return $this->context->getTitle();
146 }
147
153 public function getAction() {
154 if ( $this->action === null ) {
155 $this->action = Action::getActionName( $this->context );
156 }
157
158 return $this->action;
159 }
160
173 private function performRequest() {
174 global $wgTitle;
175
176 $request = $this->context->getRequest();
177 $requestTitle = $title = $this->context->getTitle();
178 $output = $this->context->getOutput();
179 $user = $this->context->getUser();
180
181 if ( $request->getVal( 'printable' ) === 'yes' ) {
182 $output->setPrintable();
183 }
184
185 $unused = null; // To pass it by reference
186 Hooks::run( 'BeforeInitialize', [ &$title, &$unused, &$output, &$user, $request, $this ] );
187
188 // Invalid titles. T23776: The interwikis must redirect even if the page name is empty.
189 if ( is_null( $title ) || ( $title->getDBkey() == '' && !$title->isExternal() )
190 || $title->isSpecial( 'Badtitle' )
191 ) {
192 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
193 try {
194 $this->parseTitle();
195 } catch ( MalformedTitleException $ex ) {
196 throw new BadTitleError( $ex );
197 }
198 throw new BadTitleError();
199 }
200
201 // Check user's permissions to read this page.
202 // We have to check here to catch special pages etc.
203 // We will check again in Article::view().
204 $permErrors = $title->isSpecial( 'RunJobs' )
205 ? [] // relies on HMAC key signature alone
206 : $title->getUserPermissionsErrors( 'read', $user );
207 if ( count( $permErrors ) ) {
208 // T34276: allowing the skin to generate output with $wgTitle or
209 // $this->context->title set to the input title would allow anonymous users to
210 // determine whether a page exists, potentially leaking private data. In fact, the
211 // curid and oldid request parameters would allow page titles to be enumerated even
212 // when they are not guessable. So we reset the title to Special:Badtitle before the
213 // permissions error is displayed.
214
215 // The skin mostly uses $this->context->getTitle() these days, but some extensions
216 // still use $wgTitle.
217 $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
218 $this->context->setTitle( $badTitle );
219 $wgTitle = $badTitle;
220
221 throw new PermissionsError( 'read', $permErrors );
222 }
223
224 // Interwiki redirects
225 if ( $title->isExternal() ) {
226 $rdfrom = $request->getVal( 'rdfrom' );
227 if ( $rdfrom ) {
228 $url = $title->getFullURL( [ 'rdfrom' => $rdfrom ] );
229 } else {
230 $query = $request->getValues();
231 unset( $query['title'] );
232 $url = $title->getFullURL( $query );
233 }
234 // Check for a redirect loop
235 if ( !preg_match( '/^' . preg_quote( $this->config->get( 'Server' ), '/' ) . '/', $url )
236 && $title->isLocal()
237 ) {
238 // 301 so google et al report the target as the actual url.
239 $output->redirect( $url, 301 );
240 } else {
241 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
242 try {
243 $this->parseTitle();
244 } catch ( MalformedTitleException $ex ) {
245 throw new BadTitleError( $ex );
246 }
247 throw new BadTitleError();
248 }
249 // Handle any other redirects.
250 // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
251 } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
252 // Prevent information leak via Special:MyPage et al (T109724)
253 $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
254 if ( $title->isSpecialPage() ) {
255 $specialPage = $spFactory->getPage( $title->getDBkey() );
256 if ( $specialPage instanceof RedirectSpecialPage ) {
257 $specialPage->setContext( $this->context );
258 if ( $this->config->get( 'HideIdentifiableRedirects' )
259 && $specialPage->personallyIdentifiableTarget()
260 ) {
261 list( , $subpage ) = $spFactory->resolveAlias( $title->getDBkey() );
262 $target = $specialPage->getRedirect( $subpage );
263 // Target can also be true. We let that case fall through to normal processing.
264 if ( $target instanceof Title ) {
265 if ( $target->isExternal() ) {
266 // Handle interwiki redirects
267 $target = SpecialPage::getTitleFor(
268 'GoToInterwiki',
269 'force/' . $target->getPrefixedDBkey()
270 );
271 }
272
273 $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
274 $request = new DerivativeRequest( $this->context->getRequest(), $query );
275 $request->setRequestURL( $this->context->getRequest()->getRequestURL() );
276 $this->context->setRequest( $request );
277 // Do not varnish cache these. May vary even for anons
278 $this->context->getOutput()->lowerCdnMaxage( 0 );
279 $this->context->setTitle( $target );
280 $wgTitle = $target;
281 // Reset action type cache. (Special pages have only view)
282 $this->action = null;
283 $title = $target;
284 $output->addJsConfigVars( [
285 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ),
286 ] );
287 $output->addModules( 'mediawiki.action.view.redirect' );
288 }
289 }
290 }
291 }
292
293 // Special pages ($title may have changed since if statement above)
294 if ( $title->isSpecialPage() ) {
295 // Actions that need to be made when we have a special pages
296 $spFactory->executePath( $title, $this->context );
297 } else {
298 // ...otherwise treat it as an article view. The article
299 // may still be a wikipage redirect to another article or URL.
300 $article = $this->initializeArticle();
301 if ( is_object( $article ) ) {
302 $this->performAction( $article, $requestTitle );
303 } elseif ( is_string( $article ) ) {
304 $output->redirect( $article );
305 } else {
306 throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
307 . " returned neither an object nor a URL" );
308 }
309 }
310 }
311 }
312
335 private function tryNormaliseRedirect( Title $title ) {
336 $request = $this->context->getRequest();
337 $output = $this->context->getOutput();
338
339 if ( $request->getVal( 'action', 'view' ) != 'view'
340 || $request->wasPosted()
341 || ( $request->getCheck( 'title' )
342 && $title->getPrefixedDBkey() == $request->getVal( 'title' ) )
343 || count( $request->getValueNames( [ 'action', 'title' ] ) )
344 || !Hooks::run( 'TestCanonicalRedirect', [ $request, $title, $output ] )
345 ) {
346 return false;
347 }
348
349 if ( $this->config->get( 'MainPageIsDomainRoot' ) && $request->getRequestURL() === '/' ) {
350 return false;
351 }
352
353 if ( $title->isSpecialPage() ) {
354 list( $name, $subpage ) = MediaWikiServices::getInstance()->getSpecialPageFactory()->
355 resolveAlias( $title->getDBkey() );
356 if ( $name ) {
357 $title = SpecialPage::getTitleFor( $name, $subpage );
358 }
359 }
360 // Redirect to canonical url, make it a 301 to allow caching
361 $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
362 if ( $targetUrl == $request->getFullRequestURL() ) {
363 $message = "Redirect loop detected!\n\n" .
364 "This means the wiki got confused about what page was " .
365 "requested; this sometimes happens when moving a wiki " .
366 "to a new server or changing the server configuration.\n\n";
367
368 if ( $this->config->get( 'UsePathInfo' ) ) {
369 $message .= "The wiki is trying to interpret the page " .
370 "title from the URL path portion (PATH_INFO), which " .
371 "sometimes fails depending on the web server. Try " .
372 "setting \"\$wgUsePathInfo = false;\" in your " .
373 "LocalSettings.php, or check that \$wgArticlePath " .
374 "is correct.";
375 } else {
376 $message .= "Your web server was detected as possibly not " .
377 "supporting URL path components (PATH_INFO) correctly; " .
378 "check your LocalSettings.php for a customized " .
379 "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
380 "to true.";
381 }
382 throw new HttpError( 500, $message );
383 }
384 $output->setCdnMaxage( 1200 );
385 $output->redirect( $targetUrl, '301' );
386 return true;
387 }
388
395 private function initializeArticle() {
396 $title = $this->context->getTitle();
397 if ( $this->context->canUseWikiPage() ) {
398 // Try to use request context wiki page, as there
399 // is already data from db saved in per process
400 // cache there from this->getAction() call.
401 $page = $this->context->getWikiPage();
402 } else {
403 // This case should not happen, but just in case.
404 // @TODO: remove this or use an exception
405 $page = WikiPage::factory( $title );
406 $this->context->setWikiPage( $page );
407 wfWarn( "RequestContext::canUseWikiPage() returned false" );
408 }
409
410 // Make GUI wrapper for the WikiPage
411 $article = Article::newFromWikiPage( $page, $this->context );
412
413 // Skip some unnecessary code if the content model doesn't support redirects
414 if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) {
415 return $article;
416 }
417
418 $request = $this->context->getRequest();
419
420 // Namespace might change when using redirects
421 // Check for redirects ...
422 $action = $request->getVal( 'action', 'view' );
423 $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null;
424 if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
425 && !$request->getVal( 'oldid' ) // ... and are not old revisions
426 && !$request->getVal( 'diff' ) // ... and not when showing diff
427 && $request->getVal( 'redirect' ) != 'no' // ... unless explicitly told not to
428 // ... and the article is not a non-redirect image page with associated file
429 && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
430 ) {
431 // Give extensions a change to ignore/handle redirects as needed
432 $ignoreRedirect = $target = false;
433
434 Hooks::run( 'InitializeArticleMaybeRedirect',
435 [ &$title, &$request, &$ignoreRedirect, &$target, &$article ] );
436 $page = $article->getPage(); // reflect any hook changes
437
438 // Follow redirects only for... redirects.
439 // If $target is set, then a hook wanted to redirect.
440 if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
441 // Is the target already set by an extension?
442 $target = $target ?: $page->followRedirect();
443 if ( is_string( $target ) && !$this->config->get( 'DisableHardRedirects' ) ) {
444 // we'll need to redirect
445 return $target;
446 }
447 if ( is_object( $target ) ) {
448 // Rewrite environment to redirected article
449 $rpage = WikiPage::factory( $target );
450 $rpage->loadPageData();
451 if ( $rpage->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
452 $rarticle = Article::newFromWikiPage( $rpage, $this->context );
453 $rarticle->setRedirectedFrom( $title );
454
455 $article = $rarticle;
456 $this->context->setTitle( $target );
457 $this->context->setWikiPage( $article->getPage() );
458 }
459 }
460 } else {
461 // Article may have been changed by hook
462 $this->context->setTitle( $article->getTitle() );
463 $this->context->setWikiPage( $article->getPage() );
464 }
465 }
466
467 return $article;
468 }
469
476 private function performAction( Page $page, Title $requestTitle ) {
477 $request = $this->context->getRequest();
478 $output = $this->context->getOutput();
479 $title = $this->context->getTitle();
480 $user = $this->context->getUser();
481
482 if ( !Hooks::run( 'MediaWikiPerformAction',
483 [ $output, $page, $title, $user, $request, $this ] )
484 ) {
485 return;
486 }
487
488 $act = $this->getAction();
489 $action = Action::factory( $act, $page, $this->context );
490
491 if ( $action instanceof Action ) {
492 // Narrow DB query expectations for this HTTP request
493 $trxLimits = $this->config->get( 'TrxProfilerLimits' );
494 $trxProfiler = Profiler::instance()->getTransactionProfiler();
495 if ( $request->wasPosted() && !$action->doesWrites() ) {
496 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
497 $request->markAsSafeRequest();
498 }
499
500 # Let CDN cache things if we can purge them.
501 if ( $this->config->get( 'UseCdn' ) &&
502 in_array(
503 // Use PROTO_INTERNAL because that's what getCdnUrls() uses
504 wfExpandUrl( $request->getRequestURL(), PROTO_INTERNAL ),
505 $requestTitle->getCdnUrls()
506 )
507 ) {
508 $output->setCdnMaxage( $this->config->get( 'CdnMaxAge' ) );
509 }
510
511 $action->show();
512 return;
513 }
514
515 // If we've not found out which action it is by now, it's unknown
516 $output->setStatusCode( 404 );
517 $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
518 }
519
523 public function run() {
524 try {
525 $this->setDBProfilingAgent();
526 try {
527 $this->main();
528 } catch ( ErrorPageError $e ) {
529 $out = $this->context->getOutput();
530 // TODO: Should ErrorPageError::report accept a OutputPage parameter?
532
533 // T64091: while exceptions are convenient to bubble up GUI errors,
534 // they are not internal application faults. As with normal requests, this
535 // should commit, print the output, do deferred updates, jobs, and profiling.
536 $this->doPreOutputCommit();
537 $out->output(); // display the GUI error
538 }
539 } catch ( Exception $e ) {
541 $action = $context->getRequest()->getVal( 'action', 'view' );
542 if (
543 $e instanceof DBConnectionError &&
544 $context->hasTitle() &&
545 $context->getTitle()->canExist() &&
546 in_array( $action, [ 'view', 'history' ], true ) &&
548 ) {
549 // Try to use any (even stale) file during outages...
550 $cache = new HTMLFileCache( $context->getTitle(), $action );
551 if ( $cache->isCached() ) {
552 $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE );
554 exit;
555 }
556 }
557
558 MWExceptionHandler::handleException( $e );
559 } catch ( Error $e ) {
560 // Type errors and such: at least handle it now and clean up the LBFactory state
561 MWExceptionHandler::handleException( $e );
562 }
563
564 $this->doPostOutputShutdown( 'normal' );
565 }
566
567 private function setDBProfilingAgent() {
568 $services = MediaWikiServices::getInstance();
569 // Add a comment for easy SHOW PROCESSLIST interpretation
570 $name = $this->context->getUser()->getName();
571 $services->getDBLoadBalancerFactory()->setAgentName(
572 mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) . '...' : $name
573 );
574 }
575
581 public function doPreOutputCommit( callable $postCommitWork = null ) {
582 self::preOutputCommit( $this->context, $postCommitWork );
583 }
584
596 public static function preOutputCommit(
597 IContextSource $context, callable $postCommitWork = null
598 ) {
599 $config = $context->getConfig();
600 $request = $context->getRequest();
601 $output = $context->getOutput();
602 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
603
604 // Try to make sure that all RDBMs, session, and other storage updates complete
605 ignore_user_abort( true );
606
607 // Commit all RDBMs changes from the main transaction round
608 $lbFactory->commitMasterChanges(
609 __METHOD__,
610 // Abort if any transaction was too big
611 [ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ]
612 );
613 wfDebug( __METHOD__ . ': primary transaction round committed' );
614
615 // Run updates that need to block the client or affect output (this is the last chance)
616 DeferredUpdates::doUpdates( 'run', DeferredUpdates::PRESEND );
617 wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
618 // Persist the session to avoid race conditions on subsequent requests by the client
619 $request->getSession()->save(); // T214471
620 wfDebug( __METHOD__ . ': session changes committed' );
621
622 // Figure out whether to wait for DB replication now or to use some method that assures
623 // that subsequent requests by the client will use the DB replication positions written
624 // during the shutdown() call below; the later requires working around replication lag
625 // of the store containing DB replication positions (e.g. dynomite, mcrouter).
626 list( $flags, $strategy ) = self::getChronProtStrategy( $lbFactory, $output );
627 // Record ChronologyProtector positions for DBs affected in this request at this point
628 $cpIndex = null;
629 $cpClientId = null;
630 $lbFactory->shutdown( $flags, $postCommitWork, $cpIndex, $cpClientId );
631 wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
632
633 $allowHeaders = !( $output->isDisabled() || headers_sent() );
634 if ( $cpIndex > 0 ) {
635 if ( $allowHeaders ) {
636 $now = time();
637 $expires = $now + ChronologyProtector::POSITION_COOKIE_TTL;
638 $options = [ 'prefix' => '' ];
639 $value = $lbFactory::makeCookieValueFromCPIndex( $cpIndex, $now, $cpClientId );
640 $request->response()->setCookie( 'cpPosIndex', $value, $expires, $options );
641 }
642
643 if ( $strategy === 'cookie+url' ) {
644 if ( $output->getRedirect() ) { // sanity
645 $safeUrl = $lbFactory->appendShutdownCPIndexAsQuery(
646 $output->getRedirect(),
647 $cpIndex
648 );
649 $output->redirect( $safeUrl );
650 } else {
651 $e = new LogicException( "No redirect; cannot append cpPosIndex parameter." );
652 MWExceptionHandler::logException( $e );
653 }
654 }
655 }
656
657 if ( $allowHeaders ) {
658 // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that
659 // handles this POST request (e.g. the "master" data center). Also have the user
660 // briefly bypass CDN so ChronologyProtector works for cacheable URLs.
661 if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) {
662 $expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
663 $options = [ 'prefix' => '' ];
664 $request->response()->setCookie( 'UseDC', 'master', $expires, $options );
665 $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
666 }
667
668 // Avoid letting a few seconds of replica DB lag cause a month of stale data.
669 // This logic is also intimately related to the value of $wgCdnReboundPurgeDelay.
670 if ( $lbFactory->laggedReplicaUsed() ) {
671 $maxAge = $config->get( 'CdnMaxageLagged' );
672 $output->lowerCdnMaxage( $maxAge );
673 $request->response()->header( "X-Database-Lagged: true" );
674 wfDebugLog( 'replication',
675 "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
676 }
677
678 // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
679 if ( MessageCache::singleton()->isDisabled() ) {
680 $maxAge = $config->get( 'CdnMaxageSubstitute' );
681 $output->lowerCdnMaxage( $maxAge );
682 $request->response()->header( "X-Response-Substitute: true" );
683 }
684 }
685 }
686
692 private static function getChronProtStrategy( ILBFactory $lbFactory, OutputPage $output ) {
693 // Should the client return, their request should observe the new ChronologyProtector
694 // DB positions. This request might be on a foreign wiki domain, so synchronously update
695 // the DB positions in all datacenters to be safe. If this output is not a redirect,
696 // then OutputPage::output() will be relatively slow, meaning that running it in
697 // $postCommitWork should help mask the latency of those updates.
698 $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
699 $strategy = 'cookie+sync';
700
701 $allowHeaders = !( $output->isDisabled() || headers_sent() );
702 if ( $output->getRedirect() && $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
703 // OutputPage::output() will be fast, so $postCommitWork is useless for masking
704 // the latency of synchronously updating the DB positions in all datacenters.
705 // Try to make use of the time the client spends following redirects instead.
706 $domainDistance = self::getUrlDomainDistance( $output->getRedirect() );
707 if ( $domainDistance === 'local' && $allowHeaders ) {
708 $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
709 $strategy = 'cookie'; // use same-domain cookie and keep the URL uncluttered
710 } elseif ( $domainDistance === 'remote' ) {
711 $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
712 $strategy = 'cookie+url'; // cross-domain cookie might not work
713 }
714 }
715
716 return [ $flags, $strategy ];
717 }
718
723 private static function getUrlDomainDistance( $url ) {
724 $clusterWiki = WikiMap::getWikiFromUrl( $url );
725 if ( WikiMap::isCurrentWikiId( $clusterWiki ) ) {
726 return 'local'; // the current wiki
727 }
728 if ( $clusterWiki !== false ) {
729 return 'remote'; // another wiki in this cluster/farm
730 }
731
732 return 'external';
733 }
734
745 public function doPostOutputShutdown( $mode = 'normal' ) {
746 // Record backend request timing
747 $timing = $this->context->getTiming();
748 $timing->mark( 'requestShutdown' );
749
750 // Perform the last synchronous operations...
751 try {
752 // Show visible profiling data if enabled (which cannot be post-send)
753 Profiler::instance()->logDataPageOutputOnly();
754 } catch ( Exception $e ) {
755 // An error may already have been shown in run(), so just log it to be safe
756 MWExceptionHandler::logException( $e );
757 }
758
759 // Disable WebResponse setters for post-send processing (T191537).
760 WebResponse::disableForPostSend();
761
762 $blocksHttpClient = true;
763 // Defer everything else if possible...
764 $callback = function () use ( $mode, &$blocksHttpClient ) {
765 try {
766 $this->restInPeace( $mode, $blocksHttpClient );
767 } catch ( Exception $e ) {
768 // If this is post-send, then displaying errors can cause broken HTML
769 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
770 }
771 };
772
773 if ( function_exists( 'register_postsend_function' ) ) {
774 // https://github.com/facebook/hhvm/issues/1230
775 register_postsend_function( $callback );
777 $blocksHttpClient = false;
778 } else {
779 if ( function_exists( 'fastcgi_finish_request' ) ) {
780 fastcgi_finish_request();
782 $blocksHttpClient = false;
783 } else {
784 // Either all DB and deferred updates should happen or none.
785 // The latter should not be cancelled due to client disconnect.
786 ignore_user_abort( true );
787 }
788
789 $callback();
790 }
791 }
792
793 private function main() {
794 global $wgTitle;
795
796 $output = $this->context->getOutput();
797 $request = $this->context->getRequest();
798
799 // Send Ajax requests to the Ajax dispatcher.
800 if ( $request->getVal( 'action' ) === 'ajax' ) {
801 // Set a dummy title, because $wgTitle == null might break things
802 $title = Title::makeTitle( NS_SPECIAL, 'Badtitle/performing an AJAX call in '
803 . __METHOD__
804 );
805 $this->context->setTitle( $title );
807
808 $dispatcher = new AjaxDispatcher( $this->config );
809 $dispatcher->performAction( $this->context->getUser() );
810
811 return;
812 }
813
814 // Get title from request parameters,
815 // is set on the fly by parseTitle the first time.
816 $title = $this->getTitle();
817 $action = $this->getAction();
819
820 // Set DB query expectations for this HTTP request
821 $trxLimits = $this->config->get( 'TrxProfilerLimits' );
822 $trxProfiler = Profiler::instance()->getTransactionProfiler();
823 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
824 if ( $request->hasSafeMethod() ) {
825 $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
826 } else {
827 $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
828 }
829
830 if ( $this->maybeDoHttpsRedirect() ) {
831 return;
832 }
833
834 if ( $title->canExist() && HTMLFileCache::useFileCache( $this->context ) ) {
835 // Try low-level file cache hit
837 if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
838 // Check incoming headers to see if client has this cached
839 $timestamp = $cache->cacheTimestamp();
840 if ( !$output->checkLastModified( $timestamp ) ) {
841 $cache->loadFromFileCache( $this->context );
842 }
843 // Do any stats increment/watchlist stuff, assuming user is viewing the
844 // latest revision (which should always be the case for file cache)
845 $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
846 // Tell OutputPage that output is taken care of
847 $output->disable();
848
849 return;
850 }
851 }
852
853 // Actually do the work of the request and build up any output
854 $this->performRequest();
855
856 // GUI-ify and stash the page output in MediaWiki::doPreOutputCommit() while
857 // ChronologyProtector synchronizes DB positions or replicas across all datacenters.
858 $buffer = null;
859 $outputWork = function () use ( $output, &$buffer ) {
860 if ( $buffer === null ) {
861 $buffer = $output->output( true );
862 }
863
864 return $buffer;
865 };
866
867 // Now commit any transactions, so that unreported errors after
868 // output() don't roll back the whole DB transaction and so that
869 // we avoid having both success and error text in the response
870 $this->doPreOutputCommit( $outputWork );
871
872 // Now send the actual output
873 print $outputWork();
874 }
875
882 private function shouldDoHttpRedirect() {
883 $request = $this->context->getRequest();
884
885 // Don't redirect if we're already on HTTPS
886 if ( $request->getProtocol() !== 'http' ) {
887 return false;
888 }
889
890 $force = $this->config->get( 'ForceHTTPS' );
891
892 // Don't redirect if $wgServer is explicitly HTTP. We test for this here
893 // by checking whether wfExpandUrl() is able to force HTTPS.
894 if ( !preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) ) {
895 if ( $force ) {
896 throw new RuntimeException( '$wgForceHTTPS is true but the server is not HTTPS' );
897 }
898 return false;
899 }
900
901 // Configured $wgForceHTTPS overrides the remaining conditions
902 if ( $force ) {
903 return true;
904 }
905
906 // Check if HTTPS is required by the session or user preferences
907 return $request->getSession()->shouldForceHTTPS() ||
908 // Check the cookie manually, for paranoia
909 $request->getCookie( 'forceHTTPS', '' ) ||
910 // Avoid checking the user and groups unless it's enabled.
911 (
912 $this->context->getUser()->isLoggedIn()
913 && $this->context->getUser()->requiresHTTPS()
914 );
915 }
916
926 private function maybeDoHttpsRedirect() {
927 if ( !$this->shouldDoHttpRedirect() ) {
928 return false;
929 }
930
931 $request = $this->context->getRequest();
932 $oldUrl = $request->getFullRequestURL();
933 $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
934
935 // ATTENTION: This hook is likely to be removed soon due to overall design of the system.
936 if ( !Hooks::run( 'BeforeHttpsRedirect', [ $this->context, &$redirUrl ] ) ) {
937 return false;
938 }
939
940 if ( $request->wasPosted() ) {
941 // This is weird and we'd hope it almost never happens. This
942 // means that a POST came in via HTTP and policy requires us
943 // redirecting to HTTPS. It's likely such a request is going
944 // to fail due to post data being lost, but let's try anyway
945 // and just log the instance.
946
947 // @todo FIXME: See if we could issue a 307 or 308 here, need
948 // to see how clients (automated & browser) behave when we do
949 wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
950 }
951 // Setup dummy Title, otherwise OutputPage::redirect will fail
952 $title = Title::newFromText( 'REDIR', NS_MAIN );
953 $this->context->setTitle( $title );
954 // Since we only do this redir to change proto, always send a vary header
955 $output = $this->context->getOutput();
956 $output->addVaryHeader( 'X-Forwarded-Proto' );
957 $output->redirect( $redirUrl );
958 $output->output();
959
960 return true;
961 }
962
968 public function restInPeace( $mode = 'fast', $blocksHttpClient = true ) {
969 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
970 // Assure deferred updates are not in the main transaction
971 $lbFactory->commitMasterChanges( __METHOD__ );
972
973 // Loosen DB query expectations since the HTTP client is unblocked
974 $trxProfiler = Profiler::instance()->getTransactionProfiler();
975 $trxProfiler->redefineExpectations(
976 $this->context->getRequest()->hasSafeMethod()
977 ? $this->config->get( 'TrxProfilerLimits' )['PostSend-GET']
978 : $this->config->get( 'TrxProfilerLimits' )['PostSend-POST'],
979 __METHOD__
980 );
981
982 // Do any deferred jobs; preferring to run them now if a client will not wait on them
983 DeferredUpdates::doUpdates( $blocksHttpClient ? 'enqueue' : 'run' );
984
985 // Now that everything specific to this request is done,
986 // try to occasionally run jobs (if enabled) from the queues
987 if ( $mode === 'normal' ) {
988 $this->triggerJobs();
989 }
990
991 // Log profiling data, e.g. in the database or UDP
993
994 // Commit and close up!
995 $lbFactory->commitMasterChanges( __METHOD__ );
996 $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT );
997
998 wfDebug( "Request ended normally\n" );
999 }
1000
1009 public static function emitBufferedStatsdData(
1010 IBufferingStatsdDataFactory $stats, Config $config
1011 ) {
1012 if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) {
1013 try {
1014 $statsdServer = explode( ':', $config->get( 'StatsdServer' ), 2 );
1015 $statsdHost = $statsdServer[0];
1016 $statsdPort = $statsdServer[1] ?? 8125;
1017 $statsdSender = new SocketSender( $statsdHost, $statsdPort );
1018 $statsdClient = new SamplingStatsdClient( $statsdSender, true, false );
1019 $statsdClient->setSamplingRates( $config->get( 'StatsdSamplingRates' ) );
1020 $statsdClient->send( $stats->getData() );
1021
1022 $stats->clearData(); // empty buffer for the next round
1023 } catch ( Exception $ex ) {
1024 MWExceptionHandler::logException( $ex );
1025 }
1026 }
1027 }
1028
1034 public function triggerJobs() {
1035 $jobRunRate = $this->config->get( 'JobRunRate' );
1036 if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
1037 return; // recursion guard
1038 } elseif ( $jobRunRate <= 0 || wfReadOnly() ) {
1039 return;
1040 }
1041
1042 if ( $jobRunRate < 1 ) {
1043 $max = mt_getrandmax();
1044 if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
1045 return; // the higher the job run rate, the less likely we return here
1046 }
1047 $n = 1;
1048 } else {
1049 $n = intval( $jobRunRate );
1050 }
1051
1052 $logger = LoggerFactory::getInstance( 'runJobs' );
1053
1054 try {
1055 if ( $this->config->get( 'RunJobsAsync' ) ) {
1056 // Send an HTTP request to the job RPC entry point if possible
1057 $invokedWithSuccess = $this->triggerAsyncJobs( $n, $logger );
1058 if ( !$invokedWithSuccess ) {
1059 // Fall back to blocking on running the job(s)
1060 $logger->warning( "Jobs switched to blocking; Special:RunJobs disabled" );
1061 $this->triggerSyncJobs( $n, $logger );
1062 }
1063 } else {
1064 $this->triggerSyncJobs( $n, $logger );
1065 }
1066 } catch ( JobQueueError $e ) {
1067 // Do not make the site unavailable (T88312)
1068 MWExceptionHandler::logException( $e );
1069 }
1070 }
1071
1076 private function triggerSyncJobs( $n, LoggerInterface $runJobsLogger ) {
1077 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1078 $old = $trxProfiler->setSilenced( true );
1079 try {
1080 $runner = new JobRunner( $runJobsLogger );
1081 $runner->run( [ 'maxJobs' => $n ] );
1082 } finally {
1083 $trxProfiler->setSilenced( $old );
1084 }
1085 }
1086
1092 private function triggerAsyncJobs( $n, LoggerInterface $runJobsLogger ) {
1093 // Do not send request if there are probably no jobs
1094 $group = JobQueueGroup::singleton();
1095 if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
1096 return true;
1097 }
1098
1099 $query = [ 'title' => 'Special:RunJobs',
1100 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ];
1101 $query['signature'] = SpecialRunJobs::getQuerySignature(
1102 $query, $this->config->get( 'SecretKey' ) );
1103
1104 $errno = $errstr = null;
1105 $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
1106 $host = $info ? $info['host'] : null;
1107 $port = 80;
1108 if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) {
1109 $host = "tls://" . $host;
1110 $port = 443;
1111 }
1112 if ( isset( $info['port'] ) ) {
1113 $port = $info['port'];
1114 }
1115
1116 Wikimedia\suppressWarnings();
1117 $sock = $host ? fsockopen(
1118 $host,
1119 $port,
1120 $errno,
1121 $errstr,
1122 // If it takes more than 100ms to connect to ourselves there is a problem...
1123 0.100
1124 ) : false;
1125 Wikimedia\restoreWarnings();
1126
1127 $invokedWithSuccess = true;
1128 if ( $sock ) {
1129 $special = MediaWikiServices::getInstance()->getSpecialPageFactory()->
1130 getPage( 'RunJobs' );
1131 $url = $special->getPageTitle()->getCanonicalURL( $query );
1132 $req = (
1133 "POST $url HTTP/1.1\r\n" .
1134 "Host: {$info['host']}\r\n" .
1135 "Connection: Close\r\n" .
1136 "Content-Length: 0\r\n\r\n"
1137 );
1138
1139 $runJobsLogger->info( "Running $n job(s) via '$url'" );
1140 // Send a cron API request to be performed in the background.
1141 // Give up if this takes too long to send (which should be rare).
1142 stream_set_timeout( $sock, 2 );
1143 $bytes = fwrite( $sock, $req );
1144 if ( $bytes !== strlen( $req ) ) {
1145 $invokedWithSuccess = false;
1146 $runJobsLogger->error( "Failed to start cron API (socket write error)" );
1147 } else {
1148 // Do not wait for the response (the script should handle client aborts).
1149 // Make sure that we don't close before that script reaches ignore_user_abort().
1150 $start = microtime( true );
1151 $status = fgets( $sock );
1152 $sec = microtime( true ) - $start;
1153 if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
1154 $invokedWithSuccess = false;
1155 $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" );
1156 }
1157 }
1158 fclose( $sock );
1159 } else {
1160 $invokedWithSuccess = false;
1161 $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" );
1162 }
1163
1164 return $invokedWithSuccess;
1165 }
1166}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfLogProfilingData()
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition api.php:58
Actions are things which can be done to pages (edit, delete, rollback, etc).
Definition Action.php:39
Object-Oriented Ajax functions.
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:192
Show an error page on a badtitle.
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
An error page which can definitely be safely rendered using the OutputPage.
report( $action=self::SEND_OUTPUT)
Page view caching in the file system.
static useFileCache(IContextSource $context, $mode=self::MODE_NORMAL)
Check if pages can be cached for this request/user.
Show an error that looks like an HTTP server error.
Definition HttpError.php:30
Job queue runner utility methods.
Definition JobRunner.php:39
static getHTML( $e)
If $wgShowExceptionDetails is true, return a HTML message with a backtrace to the error,...
MediaWiki exception.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
parseTitle()
Parse the request to get the Title object.
Definition MediaWiki.php:68
static emitBufferedStatsdData(IBufferingStatsdDataFactory $stats, Config $config)
Send out any buffered statsd data according to sampling rules.
static getChronProtStrategy(ILBFactory $lbFactory, OutputPage $output)
triggerAsyncJobs( $n, LoggerInterface $runJobsLogger)
doPostOutputShutdown( $mode='normal')
This function does work that can be done after the user gets the HTTP response so they don't block on...
initializeArticle()
Initialize the main Article object for "standard" actions (view, etc) Create an Article object for th...
Config $config
Definition MediaWiki.php:43
run()
Run the current MediaWiki instance; index.php just calls this.
maybeDoHttpsRedirect()
If the stars are suitably aligned, do an HTTP->HTTPS redirect.
getTitle()
Get the Title object that we'll be acting on, as specified in the WebRequest.
__construct(IContextSource $context=null)
Definition MediaWiki.php:53
triggerSyncJobs( $n, LoggerInterface $runJobsLogger)
getAction()
Returns the name of the action that will be executed.
string $action
Cache what action this request is.
Definition MediaWiki.php:48
restInPeace( $mode='fast', $blocksHttpClient=true)
Ends this task peacefully.
tryNormaliseRedirect(Title $title)
Handle redirects for uncanonical title requests.
static preOutputCommit(IContextSource $context, callable $postCommitWork=null)
This function commits all DB and session changes as needed before the client can receive a response (...
IContextSource $context
Definition MediaWiki.php:38
triggerJobs()
Potentially open a socket and sent an HTTP request back to the server to run a specified number of jo...
performRequest()
Performs the request.
static getUrlDomainDistance( $url)
setDBProfilingAgent()
doPreOutputCommit(callable $postCommitWork=null)
performAction(Page $page, Title $requestTitle)
Perform one of the "standard" actions.
shouldDoHttpRedirect()
Check if an HTTP->HTTPS redirect should be done.
static singleton()
Get the singleton instance of this class.
This is one of the Core classes and should be read at least once by any new developers.
getRedirect()
Get the URL to redirect to, or an empty string if not redirect URL set.
isDisabled()
Return whether the output will be completely disabled.
Show an error when a user tries to do something they do not have the necessary permissions for.
Shortcut to construct a special page alias.
A statsd client that applies the sampling rate to the data items before sending them.
static getQuerySignature(array $query, $secretKey)
Represents a title within MediaWiki.
Definition Title.php:42
getCdnUrls()
Get a list of URLs to purge from the CDN cache when this page changes.
Definition Title.php:3504
Special handling for file pages.
Helper class for mitigating DB replication lag in order to provide "session consistency".
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:64
const PROTO_HTTPS
Definition Defines.php:209
const NS_FILE
Definition Defines.php:75
const PROTO_CURRENT
Definition Defines.php:211
const NS_MAIN
Definition Defines.php:69
const PROTO_INTERNAL
Definition Defines.php:213
const NS_SPECIAL
Definition Defines.php:58
const NS_MEDIA
Definition Defines.php:57
Interface for configuration instances.
Definition Config.php:28
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
hasData()
Check whether this data factory has any buffered data.
clearData()
Clear all buffered data from the factory.
getData()
Return the buffered data from the factory.
Interface for objects which can provide a MediaWiki context on request.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:29
An interface for generating database load balancers.
hasOrMadeRecentMasterChanges( $age=null)
Determine if any master connection has pending/written changes from this request.
$context
Definition load.php:45
$cache
Definition mcc.php:33
This class serves as a utility class for this extension.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42