23 use Liuggio\StatsdClient\Sender\SocketSender;
24 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
27 use Psr\Log\LoggerInterface;
37 use ProtectedHookAccessorTrait;
50 private const DEFER_FASTCGI_FINISH_REQUEST = 1;
52 private const DEFER_SET_LENGTH_AND_FLUSH = 2;
59 $this->config = $this->context->getConfig();
60 if ( function_exists(
'fastcgi_finish_request' ) ) {
61 $this->postSendStrategy = self::DEFER_FASTCGI_FINISH_REQUEST;
63 $this->postSendStrategy = self::DEFER_SET_LENGTH_AND_FLUSH;
74 $request = $this->context->getRequest();
75 $curid = $request->getInt(
'curid' );
76 $title = $request->getVal(
'title' );
77 $action = $request->getVal(
'action' );
79 if ( $request->getCheck(
'search' ) ) {
89 if ( $ret !==
null ) {
92 if ( $ret->getNamespace() ===
NS_MEDIA ) {
98 $languageConverter = $services
99 ->getLanguageConverterFactory()
100 ->getLanguageConverter( $services->getContentLanguage() );
101 if ( $languageConverter->hasVariants() && !$ret->exists() ) {
102 $languageConverter->findVariantLink(
$title, $ret );
111 if ( $ret ===
null || !$ret->isSpecialPage() ) {
113 $oldid = $request->getInt(
'oldid' );
114 $oldid = $oldid ?: $request->getInt(
'diff' );
118 ->getRevisionLookup()
119 ->getRevisionById( $oldid );
122 $revRecord->getPageAsLinkTarget()
130 && strval(
$title ) ===
''
131 && !$request->getCheck(
'curid' )
137 if ( $ret ===
null || ( $ret->getDBkey() ==
'' && !$ret->isExternal() ) ) {
152 if ( !$this->context->hasTitle() ) {
154 $this->context->setTitle( $this->
parseTitle() );
159 return $this->context->getTitle();
168 if ( $this->action ===
null ) {
190 $request = $this->context->getRequest();
191 $requestTitle =
$title = $this->context->getTitle();
192 $output = $this->context->getOutput();
193 $user = $this->context->getUser();
195 if ( $request->getVal(
'printable' ) ===
'yes' ) {
196 $output->setPrintable();
199 $this->getHookRunner()->onBeforeInitialize(
$title,
null, $output, $user, $request, $this );
203 ||
$title->isSpecial(
'Badtitle' )
217 $permErrors =
$title->isSpecial(
'RunJobs' )
220 ->getPermissionErrors(
'read', $user,
$title );
221 if ( count( $permErrors ) ) {
232 $this->context->setTitle( $badTitle );
239 if (
$title->isExternal() ) {
240 $rdfrom = $request->getVal(
'rdfrom' );
242 $url =
$title->getFullURL( [
'rdfrom' => $rdfrom ] );
244 $query = $request->getValues();
245 unset( $query[
'title'] );
246 $url =
$title->getFullURL( $query );
249 if ( !preg_match(
'/^' . preg_quote( $this->config->get(
'Server' ),
'/' ) .
'/', $url )
253 $output->redirect( $url, 301 );
268 if (
$title->isSpecialPage() ) {
269 $specialPage = $spFactory->getPage(
$title->getDBkey() );
271 $specialPage->setContext( $this->context );
272 if ( $this->config->get(
'HideIdentifiableRedirects' )
273 && $specialPage->personallyIdentifiableTarget()
275 list( , $subpage ) = $spFactory->resolveAlias(
$title->getDBkey() );
276 $target = $specialPage->getRedirect( $subpage );
278 if ( $target instanceof
Title ) {
279 if ( $target->isExternal() ) {
283 'force/' . $target->getPrefixedDBkey()
287 $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
289 $request->setRequestURL( $this->context->getRequest()->getRequestURL() );
290 $this->context->setRequest( $request );
292 $this->context->getOutput()->lowerCdnMaxage( 0 );
293 $this->context->setTitle( $target );
296 $this->action =
null;
298 $output->addJsConfigVars( [
299 'wgInternalRedirectTargetUrl' => $target->getLinkURL( $query ),
301 $output->addModules(
'mediawiki.action.view.redirect' );
308 if (
$title->isSpecialPage() ) {
310 $spFactory->executePath(
$title, $this->context );
315 if ( is_object( $article ) ) {
317 } elseif ( is_string( $article ) ) {
318 $output->redirect( $article );
320 throw new MWException(
"Shouldn't happen: MediaWiki::initializeArticle()"
321 .
" returned neither an object nor a URL" );
324 $output->considerCacheSettingsFinal();
351 $request = $this->context->getRequest();
352 $output = $this->context->getOutput();
354 if ( $request->getVal(
'action',
'view' ) !=
'view'
355 || $request->wasPosted()
356 || ( $request->getCheck(
'title' )
357 &&
$title->getPrefixedDBkey() == $request->getVal(
'title' ) )
358 || count( $request->getValueNames( [
'action',
'title' ] ) )
359 || !$this->getHookRunner()->onTestCanonicalRedirect( $request,
$title, $output )
364 if ( $this->config->get(
'MainPageIsDomainRoot' ) && $request->getRequestURL() ===
'/' ) {
368 if (
$title->isSpecialPage() ) {
370 resolveAlias(
$title->getDBkey() );
377 if ( $targetUrl == $request->getFullRequestURL() ) {
378 $message =
"Redirect loop detected!\n\n" .
379 "This means the wiki got confused about what page was " .
380 "requested; this sometimes happens when moving a wiki " .
381 "to a new server or changing the server configuration.\n\n";
383 if ( $this->config->get(
'UsePathInfo' ) ) {
384 $message .=
"The wiki is trying to interpret the page " .
385 "title from the URL path portion (PATH_INFO), which " .
386 "sometimes fails depending on the web server. Try " .
387 "setting \"\$wgUsePathInfo = false;\" in your " .
388 "LocalSettings.php, or check that \$wgArticlePath " .
391 $message .=
"Your web server was detected as possibly not " .
392 "supporting URL path components (PATH_INFO) correctly; " .
393 "check your LocalSettings.php for a customized " .
394 "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
399 $output->setCdnMaxage( 1200 );
400 $output->redirect( $targetUrl,
'301' );
411 $title = $this->context->getTitle();
413 if ( $this->context->canUseWikiPage() ) {
417 $page = $this->context->getWikiPage();
421 $page = $services->getWikiPageFactory()->newFromTitle(
$title );
422 $this->context->setWikiPage( $page );
423 wfWarn(
"RequestContext::canUseWikiPage() returned false" );
430 if ( !$services->getContentHandlerFactory()
431 ->getContentHandler(
$title->getContentModel() )
432 ->supportsRedirects()
437 $request = $this->context->getRequest();
441 $action = $request->getVal(
'action',
'view' );
444 && !$request->getVal(
'oldid' )
445 && !$request->getVal(
'diff' )
446 && $request->getVal(
'redirect' ) !=
'no'
448 && !( is_object(
$file ) &&
$file->exists() && !
$file->getRedirected() )
451 $ignoreRedirect = $target =
false;
453 $this->getHookRunner()->onInitializeArticleMaybeRedirect(
$title, $request,
454 $ignoreRedirect, $target, $article );
455 $page = $article->getPage();
459 if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
461 $target = $target ?: $page->followRedirect();
462 if ( is_string( $target ) && !$this->config->get(
'DisableHardRedirects' ) ) {
466 if ( is_object( $target ) ) {
468 $rpage = $services->getWikiPageFactory()->newFromTitle( $target );
469 $rpage->loadPageData();
470 if ( $rpage->exists() || ( is_object(
$file ) && !
$file->isLocal() ) ) {
472 $rarticle->setRedirectedFrom(
$title );
474 $article = $rarticle;
475 $this->context->setTitle( $target );
476 $this->context->setWikiPage( $article->getPage() );
481 $this->context->setTitle( $article->getTitle() );
482 $this->context->setWikiPage( $article->getPage() );
496 $request = $this->context->getRequest();
497 $output = $this->context->getOutput();
498 $title = $this->context->getTitle();
499 $user = $this->context->getUser();
501 if ( !$this->getHookRunner()->onMediaWikiPerformAction(
502 $output, $article,
$title, $user, $request, $this )
512 $trxLimits = $this->config->get(
'TrxProfilerLimits' );
514 if ( $request->wasPosted() && !
$action->doesWrites() ) {
515 $trxProfiler->setExpectations( $trxLimits[
'POST-nonwrite'], __METHOD__ );
516 $request->markAsSafeRequest();
519 # Let CDN cache things if we can purge them.
520 if ( $this->config->get(
'UseCdn' ) ) {
525 $htmlCacheUpdater->getUrls( $requestTitle )
528 $output->setCdnMaxage( $this->config->get(
'CdnMaxAge' ) );
537 $output->setStatusCode( 404 );
538 $output->showErrorPage(
'nosuchaction',
'nosuchactiontext' );
550 $out = $this->context->getOutput();
553 $out->considerCacheSettingsFinal();
561 }
catch ( Exception $e ) {
568 in_array(
$action, [
'view',
'history' ],
true ) &&
573 if (
$cache->isCached() ) {
582 }
catch ( Throwable $e ) {
595 $name = $this->context->getUser()->getName();
596 $services->getDBLoadBalancerFactory()->setAgentName(
597 mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) .
'...' : $name
605 $jobRunRate = $this->config->get(
'JobRunRate' );
608 $this->
getTitle()->isSpecial(
'RunJobs' ) ||
613 $this->context->getRequest()->getMethod() ===
'HEAD' ||
614 $this->context->getRequest()->getHeader(
'If-Modified-Since' )
620 if ( $jobRunRate < 1 ) {
621 $max = mt_getrandmax();
622 if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
627 $n = intval( $jobRunRate );
632 $logger = LoggerFactory::getInstance(
'runJobs' );
633 if ( $this->config->get(
'RunJobsAsync' ) ) {
636 if ( !$invokedWithSuccess ) {
638 $logger->warning(
"Jobs switched to blocking; Special:RunJobs disabled" );
674 $lbFactory = $services->getDBLoadBalancerFactory();
677 ignore_user_abort(
true );
680 $lbFactory->commitMasterChanges(
683 [
'maxWriteDuration' =>
$config->
get(
'MaxUserDBWriteDuration' ) ]
685 wfDebug( __METHOD__ .
': primary transaction round committed' );
689 wfDebug( __METHOD__ .
': pre-send deferred updates completed' );
691 $request->getSession()->save();
692 wfDebug( __METHOD__ .
': session changes committed' );
702 $lbFactory->shutdown( $flags, $postCommitWork, $cpIndex, $cpClientId );
704 $allowHeaders = !( $output->isDisabled() || headers_sent() );
705 if ( $cpIndex > 0 ) {
706 if ( $allowHeaders ) {
708 $expires = $now + ChronologyProtector::POSITION_COOKIE_TTL;
709 $options = [
'prefix' =>
'' ];
710 $value = $lbFactory::makeCookieValueFromCPIndex( $cpIndex, $now, $cpClientId );
711 $request->response()->setCookie(
'cpPosIndex', $value, $expires, $options );
714 if ( $strategy ===
'cookie+url' ) {
715 if ( $output->getRedirect() ) {
716 $safeUrl = $lbFactory->appendShutdownCPIndexAsQuery(
717 $output->getRedirect(),
720 $output->redirect( $safeUrl );
723 new LogicException(
"No redirect; cannot append cpPosIndex parameter." ),
724 MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
730 if ( $allowHeaders ) {
734 if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) {
735 $expires = time() +
$config->
get(
'DataCenterUpdateStickTTL' );
736 $options = [
'prefix' =>
'' ];
737 $request->response()->setCookie(
'UseDC',
'master', $expires, $options );
738 $request->response()->setCookie(
'UseCDNCache',
'false', $expires, $options );
743 if ( $lbFactory->laggedReplicaUsed() ) {
745 $output->lowerCdnMaxage( $maxAge );
746 $request->response()->header(
"X-Database-Lagged: true" );
748 "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
752 if ( $services->getMessageCache()->isDisabled() ) {
753 $maxAge =
$config->
get(
'CdnMaxageSubstitute' );
754 $output->lowerCdnMaxage( $maxAge );
755 $request->response()->header(
"X-Response-Substitute: true" );
758 if ( !$output->couldBePublicCached() || $output->haveCacheVaryCookies() ) {
770 $services->getBlockManager()
771 ->trackBlockWithCookie( $user, $request->response() );
787 $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
788 $strategy =
'cookie+sync';
790 $allowHeaders = !( $output->
isDisabled() || headers_sent() );
796 if ( $domainDistance ===
'local' && $allowHeaders ) {
797 $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
798 $strategy =
'cookie';
799 } elseif ( $domainDistance ===
'remote' ) {
800 $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
801 $strategy =
'cookie+url';
805 return [ $flags, $strategy ];
817 if ( $clusterWiki !==
false ) {
835 $timing = $this->context->getTiming();
836 $timing->mark(
'requestShutdown' );
842 }
catch ( Throwable $e ) {
851 $callback =
function () {
854 }
catch ( Throwable $e ) {
858 MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
863 if ( $this->postSendStrategy === self::DEFER_FASTCGI_FINISH_REQUEST ) {
864 fastcgi_finish_request();
868 if ( !$this->config->get(
'CommandLineMode' ) ) {
869 AtEase\AtEase::suppressWarnings();
870 if ( ob_get_status() ) {
874 AtEase\AtEase::restoreWarnings();
886 $output = $this->context->getOutput();
887 $request = $this->context->getRequest();
890 if ( $request->getVal(
'action' ) ===
'ajax' ) {
895 $this->context->setTitle(
$title );
899 $dispatcher->performAction( $this->context->getUser() );
911 $trxLimits = $this->config->get(
'TrxProfilerLimits' );
913 $trxProfiler->setLogger( LoggerFactory::getInstance(
'DBPerformance' ) );
914 if ( $request->hasSafeMethod() ) {
915 $trxProfiler->setExpectations( $trxLimits[
'GET'], __METHOD__ );
917 $trxProfiler->setExpectations( $trxLimits[
'POST'], __METHOD__ );
927 if (
$cache->isCacheGood( ) ) {
929 $timestamp =
$cache->cacheTimestamp();
930 if ( !$output->checkLastModified( $timestamp ) ) {
931 $cache->loadFromFileCache( $this->context );
935 $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
949 $outputWork =
function () use ( $output, &$buffer ) {
950 if ( $buffer ===
null ) {
951 $buffer = $output->output(
true );
974 $request = $this->context->getRequest();
977 if ( $request->getProtocol() !==
'http' ) {
981 $force = $this->config->get(
'ForceHTTPS' );
987 throw new RuntimeException(
'$wgForceHTTPS is true but the server is not HTTPS' );
998 return $request->getSession()->shouldForceHTTPS() ||
1000 $request->getCookie(
'forceHTTPS',
'' ) ||
1003 $this->context->getUser()->isRegistered()
1004 && $this->context->getUser()->requiresHTTPS()
1022 $request = $this->context->getRequest();
1023 $oldUrl = $request->getFullRequestURL();
1024 $redirUrl = preg_replace(
'#^http://#',
'https://', $oldUrl );
1027 if ( !$this->getHookRunner()->onBeforeHttpsRedirect( $this->context, $redirUrl ) ) {
1031 if ( $request->wasPosted() ) {
1040 wfDebugLog(
'RedirectedPosts',
"Redirected from HTTP to HTTPS: $oldUrl" );
1044 $this->context->setTitle(
$title );
1046 $output = $this->context->getOutput();
1047 $output->addVaryHeader(
'X-Forwarded-Proto' );
1048 $output->redirect( $redirUrl );
1063 $this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH &&
1067 $response = $this->context->getRequest()->response();
1069 $response->header(
'Connection: close' );
1073 $response->header(
'Content-Encoding: identity' );
1074 AtEase\AtEase::suppressWarnings();
1075 ini_set(
'zlib.output_compression', 0 );
1076 if ( function_exists(
'apache_setenv' ) ) {
1077 apache_setenv(
'no-gzip',
'1' );
1079 AtEase\AtEase::restoreWarnings();
1083 if ( !$this->context->getOutput()->isDisabled() ) {
1086 $response->header(
'Content-Length: ' . ob_get_length() );
1101 ignore_user_abort(
true );
1105 $lbFactory->commitMasterChanges( __METHOD__ );
1109 $trxProfiler->redefineExpectations(
1110 $this->context->getRequest()->hasSafeMethod()
1111 ? $this->config->get(
'TrxProfilerLimits' )[
'PostSend-GET']
1112 : $this->config->get(
'TrxProfilerLimits' )[
'PostSend-POST'],
1123 $lbFactory->commitMasterChanges( __METHOD__ );
1124 $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT );
1126 wfDebug(
"Request ended normally" );
1142 $statsdServer = explode(
':',
$config->
get(
'StatsdServer' ), 2 );
1143 $statsdHost = $statsdServer[0];
1144 $statsdPort = $statsdServer[1] ?? 8125;
1145 $statsdSender =
new SocketSender( $statsdHost, $statsdPort );
1147 $statsdClient->setSamplingRates(
$config->
get(
'StatsdSamplingRates' ) );
1148 $statsdClient->send( $stats->
getData() );
1151 }
catch ( Exception $e ) {
1164 $jobRunRate = $this->config->get(
'JobRunRate' );
1165 if ( $this->
getTitle()->isSpecial(
'RunJobs' ) ) {
1167 } elseif ( $jobRunRate <= 0 ||
wfReadOnly() ) {
1171 if ( $jobRunRate < 1 ) {
1172 $max = mt_getrandmax();
1173 if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
1178 $n = intval( $jobRunRate );
1181 $logger = LoggerFactory::getInstance(
'runJobs' );
1184 if ( $this->config->get(
'RunJobsAsync' ) ) {
1187 if ( !$invokedWithSuccess ) {
1189 $logger->warning(
"Jobs switched to blocking; Special:RunJobs disabled" );
1206 $old = $trxProfiler->setSilenced(
true );
1209 $runner->run( [
'maxJobs' => $n ] );
1211 $trxProfiler->setSilenced( $old );
1227 $query = [
'title' =>
'Special:RunJobs',
1228 'tasks' =>
'jobs',
'maxjobs' => $n,
'sigexpiry' => time() + 5 ];
1230 $query, $this->config->get(
'SecretKey' ) );
1232 $errno = $errstr =
null;
1233 $info =
wfParseUrl( $this->config->get(
'CanonicalServer' ) );
1234 $host = $info ? $info[
'host'] :
null;
1236 if ( isset( $info[
'scheme'] ) && $info[
'scheme'] ==
'https' ) {
1237 $host =
"tls://" . $host;
1240 if ( isset( $info[
'port'] ) ) {
1241 $port = $info[
'port'];
1244 Wikimedia\suppressWarnings();
1245 $sock = $host ? fsockopen(
1253 Wikimedia\restoreWarnings();
1255 $invokedWithSuccess =
true;
1258 getPage(
'RunJobs' );
1259 $url = $special->getPageTitle()->getCanonicalURL( $query );
1261 "POST $url HTTP/1.1\r\n" .
1262 "Host: {$info['host']}\r\n" .
1263 "Connection: Close\r\n" .
1264 "Content-Length: 0\r\n\r\n"
1267 $runJobsLogger->info(
"Running $n job(s) via '$url'" );
1270 stream_set_timeout( $sock, 2 );
1271 $bytes = fwrite( $sock, $req );
1272 if ( $bytes !== strlen( $req ) ) {
1273 $invokedWithSuccess =
false;
1274 $runJobsLogger->error(
"Failed to start cron API (socket write error)" );
1278 $start = microtime(
true );
1279 $status = fgets( $sock );
1280 $sec = microtime(
true ) - $start;
1281 if ( !preg_match(
'#^HTTP/\d\.\d 202 #', $status ) ) {
1282 $invokedWithSuccess =
false;
1283 $runJobsLogger->error(
"Failed to start cron API: received '$status' ($sec)" );
1288 $invokedWithSuccess =
false;
1289 $runJobsLogger->error(
"Failed to start cron API (socket error $errno): $errstr" );
1292 return $invokedWithSuccess;