24 use Psr\Log\LoggerInterface;
29 use Liuggio\StatsdClient\Sender\SocketSender;
69 $request = $this->context->getRequest();
70 $curid = $request->getInt(
'curid' );
71 $title = $request->getVal(
'title' );
72 $action = $request->getVal(
'action' );
74 if ( $request->getCheck(
'search' ) ) {
86 if ( !is_null( $ret ) && $ret->getNamespace() ==
NS_MEDIA ) {
93 $contLang->hasVariants() && !is_null( $ret ) && $ret->getArticleID() == 0
95 $contLang->findVariantLink(
$title, $ret );
103 if ( $ret ===
null || !$ret->isSpecialPage() ) {
105 $oldid = $request->getInt(
'oldid' );
106 $oldid = $oldid ?: $request->getInt(
'diff' );
110 $ret = $rev ? $rev->getTitle() : $ret;
116 && strval(
$title ) ===
''
117 && !$request->getCheck(
'curid' )
123 if ( $ret ===
null || ( $ret->getDBkey() ==
'' && !$ret->isExternal() ) ) {
138 if ( !$this->context->hasTitle() ) {
140 $this->context->setTitle( $this->
parseTitle() );
145 return $this->context->getTitle();
154 if ( $this->action ===
null ) {
176 $request = $this->context->getRequest();
177 $requestTitle =
$title = $this->context->getTitle();
178 $output = $this->context->getOutput();
179 $user = $this->context->getUser();
181 if ( $request->getVal(
'printable' ) ===
'yes' ) {
190 ||
$title->isSpecial(
'Badtitle' )
204 $permErrors =
$title->isSpecial(
'RunJobs' )
206 :
$title->getUserPermissionsErrors(
'read', $user );
207 if ( count( $permErrors ) ) {
218 $this->context->setTitle( $badTitle );
225 if (
$title->isExternal() ) {
226 $rdfrom = $request->getVal(
'rdfrom' );
228 $url =
$title->getFullURL( [
'rdfrom' => $rdfrom ] );
230 $query = $request->getValues();
231 unset( $query[
'title'] );
232 $url =
$title->getFullURL( $query );
235 if ( !preg_match(
'/^' . preg_quote( $this->config->get(
'Server' ),
'/' ) .
'/', $url )
239 $output->redirect( $url, 301 );
254 if (
$title->isSpecialPage() ) {
255 $specialPage = $spFactory->getPage(
$title->getDBkey() );
257 $specialPage->setContext( $this->context );
258 if ( $this->config->get(
'HideIdentifiableRedirects' )
259 && $specialPage->personallyIdentifiableTarget()
261 list( , $subpage ) = $spFactory->resolveAlias(
$title->getDBkey() );
262 $target = $specialPage->getRedirect( $subpage );
264 if ( $target instanceof
Title ) {
265 if ( $target->isExternal() ) {
269 'force/' . $target->getPrefixedDBkey()
273 $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
275 $request->
setRequestURL( $this->context->getRequest()->getRequestURL() );
276 $this->context->setRequest( $request );
278 $this->context->getOutput()->lowerCdnMaxage( 0 );
279 $this->context->setTitle( $target );
282 $this->action =
null;
285 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ),
287 $output->addModules(
'mediawiki.action.view.redirect' );
294 if (
$title->isSpecialPage() ) {
296 $spFactory->executePath(
$title, $this->context );
301 if ( is_object( $article ) ) {
303 } elseif ( is_string( $article ) ) {
306 throw new MWException(
"Shouldn't happen: MediaWiki::initializeArticle()"
307 .
" returned neither an object nor a URL" );
336 $request = $this->context->getRequest();
337 $output = $this->context->getOutput();
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' ] ) )
349 if ( $this->config->get(
'MainPageIsDomainRoot' ) && $request->getRequestURL() ===
'/' ) {
353 if (
$title->isSpecialPage() ) {
355 resolveAlias(
$title->getDBkey() );
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";
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 " .
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 " .
385 $output->redirect( $targetUrl,
'301' );
396 $title = $this->context->getTitle();
397 if ( $this->context->canUseWikiPage() ) {
401 $page = $this->context->getWikiPage();
406 $this->context->setWikiPage( $page );
407 wfWarn(
"RequestContext::canUseWikiPage() returned false" );
418 $request = $this->context->getRequest();
422 $action = $request->getVal(
'action',
'view' );
425 && !$request->getVal(
'oldid' )
426 && !$request->getVal(
'diff' )
427 && $request->getVal(
'redirect' ) !=
'no'
429 && !( is_object(
$file ) &&
$file->exists() && !
$file->getRedirected() )
432 $ignoreRedirect = $target =
false;
435 [ &
$title, &$request, &$ignoreRedirect, &$target, &$article ] );
436 $page = $article->getPage();
440 if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
442 $target = $target ?: $page->followRedirect();
443 if ( is_string( $target ) && !$this->config->get(
'DisableHardRedirects' ) ) {
447 if ( is_object( $target ) ) {
450 $rpage->loadPageData();
451 if ( $rpage->exists() || ( is_object(
$file ) && !
$file->isLocal() ) ) {
453 $rarticle->setRedirectedFrom(
$title );
455 $article = $rarticle;
456 $this->context->setTitle( $target );
457 $this->context->setWikiPage( $article->getPage() );
462 $this->context->setTitle( $article->getTitle() );
463 $this->context->setWikiPage( $article->getPage() );
477 $request = $this->context->getRequest();
478 $output = $this->context->getOutput();
479 $title = $this->context->getTitle();
480 $user = $this->context->getUser();
493 $trxLimits = $this->config->get(
'TrxProfilerLimits' );
495 if ( $request->wasPosted() && !
$action->doesWrites() ) {
496 $trxProfiler->setExpectations( $trxLimits[
'POST-nonwrite'], __METHOD__ );
497 $request->markAsSafeRequest();
500 # Let CDN cache things if we can purge them.
501 if ( $this->config->get(
'UseCdn' ) &&
508 $output->setCdnMaxage( $this->config->get(
'CdnMaxAge' ) );
517 $output->showErrorPage(
'nosuchaction',
'nosuchactiontext' );
529 $out = $this->context->getOutput();
539 }
catch ( Exception $e ) {
546 in_array(
$action, [
'view',
'history' ],
true ) &&
551 if (
$cache->isCached() ) {
559 }
catch ( Error $e ) {
570 $name = $this->context->getUser()->getName();
571 $services->getDBLoadBalancerFactory()->setAgentName(
572 mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) .
'...' : $name
605 ignore_user_abort(
true );
608 $lbFactory->commitMasterChanges(
611 [
'maxWriteDuration' =>
$config->
get(
'MaxUserDBWriteDuration' ) ]
613 wfDebug( __METHOD__ .
': primary transaction round committed' );
617 wfDebug( __METHOD__ .
': pre-send deferred updates completed' );
619 $request->getSession()->save();
620 wfDebug( __METHOD__ .
': session changes committed' );
630 $lbFactory->shutdown( $flags, $postCommitWork, $cpIndex, $cpClientId );
631 wfDebug( __METHOD__ .
': LBFactory shutdown completed' );
633 $allowHeaders = !(
$output->isDisabled() || headers_sent() );
634 if ( $cpIndex > 0 ) {
635 if ( $allowHeaders ) {
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 );
643 if ( $strategy ===
'cookie+url' ) {
644 if (
$output->getRedirect() ) {
645 $safeUrl = $lbFactory->appendShutdownCPIndexAsQuery(
651 $e =
new LogicException(
"No redirect; cannot append cpPosIndex parameter." );
657 if ( $allowHeaders ) {
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 );
670 if ( $lbFactory->laggedReplicaUsed() ) {
672 $output->lowerCdnMaxage( $maxAge );
673 $request->response()->header(
"X-Database-Lagged: true" );
675 "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
680 $maxAge =
$config->
get(
'CdnMaxageSubstitute' );
681 $output->lowerCdnMaxage( $maxAge );
682 $request->response()->header(
"X-Response-Substitute: true" );
698 $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
699 $strategy =
'cookie+sync';
701 $allowHeaders = !(
$output->isDisabled() || headers_sent() );
707 if ( $domainDistance ===
'local' && $allowHeaders ) {
708 $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
709 $strategy =
'cookie';
710 } elseif ( $domainDistance ===
'remote' ) {
711 $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
712 $strategy =
'cookie+url';
716 return [ $flags, $strategy ];
728 if ( $clusterWiki !==
false ) {
747 $timing = $this->context->getTiming();
748 $timing->mark(
'requestShutdown' );
754 }
catch ( Exception $e ) {
762 $blocksHttpClient =
true;
764 $callback =
function () use ( $mode, &$blocksHttpClient ) {
767 }
catch ( Exception $e ) {
773 if ( function_exists(
'register_postsend_function' ) ) {
775 register_postsend_function( $callback );
777 $blocksHttpClient =
false;
779 if ( function_exists(
'fastcgi_finish_request' ) ) {
780 fastcgi_finish_request();
782 $blocksHttpClient =
false;
786 ignore_user_abort(
true );
796 $output = $this->context->getOutput();
797 $request = $this->context->getRequest();
800 if ( $request->getVal(
'action' ) ===
'ajax' ) {
805 $this->context->setTitle(
$title );
809 $dispatcher->performAction( $this->context->getUser() );
821 $trxLimits = $this->config->get(
'TrxProfilerLimits' );
823 $trxProfiler->setLogger( LoggerFactory::getInstance(
'DBPerformance' ) );
824 if ( $request->hasSafeMethod() ) {
825 $trxProfiler->setExpectations( $trxLimits[
'GET'], __METHOD__ );
827 $trxProfiler->setExpectations( $trxLimits[
'POST'], __METHOD__ );
836 $request->getProtocol() ==
'http' &&
840 $request->getSession()->shouldForceHTTPS() ||
842 $request->getCookie(
'forceHTTPS',
'' ) ||
844 $request->getCookie(
'forceHTTPS' ) ||
847 $this->context->getUser()->isLoggedIn()
848 && $this->context->getUser()->requiresHTTPS()
852 $oldUrl = $request->getFullRequestURL();
853 $redirUrl = preg_replace(
'#^http://#',
'https://', $oldUrl );
856 if (
Hooks::run(
'BeforeHttpsRedirect', [ $this->context, &$redirUrl ] ) ) {
857 if ( $request->wasPosted() ) {
866 wfDebugLog(
'RedirectedPosts',
"Redirected from HTTP to HTTPS: $oldUrl" );
870 $this->context->setTitle(
$title );
872 $output->addVaryHeader(
'X-Forwarded-Proto' );
873 $output->redirect( $redirUrl );
883 if (
$cache->isCacheGood( ) ) {
885 $timestamp =
$cache->cacheTimestamp();
886 if ( !
$output->checkLastModified( $timestamp ) ) {
887 $cache->loadFromFileCache( $this->context );
891 $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
905 $outputWork =
function () use (
$output, &$buffer ) {
906 if ( $buffer ===
null ) {
907 $buffer =
$output->output(
true );
927 public function restInPeace( $mode =
'fast', $blocksHttpClient =
true ) {
930 $lbFactory->commitMasterChanges( __METHOD__ );
934 $trxProfiler->redefineExpectations(
935 $this->context->getRequest()->hasSafeMethod()
936 ? $this->config->get(
'TrxProfilerLimits' )[
'PostSend-GET']
937 : $this->config->get(
'TrxProfilerLimits' )[
'PostSend-POST'],
946 if ( $mode ===
'normal' ) {
954 $lbFactory->commitMasterChanges( __METHOD__ );
955 $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT );
957 wfDebug(
"Request ended normally\n" );
973 $statsdServer = explode(
':',
$config->
get(
'StatsdServer' ), 2 );
974 $statsdHost = $statsdServer[0];
975 $statsdPort = $statsdServer[1] ?? 8125;
976 $statsdSender =
new SocketSender( $statsdHost, $statsdPort );
978 $statsdClient->setSamplingRates(
$config->
get(
'StatsdSamplingRates' ) );
979 $statsdClient->send( $stats->
getData() );
982 }
catch ( Exception $ex ) {
994 $jobRunRate = $this->config->get(
'JobRunRate' );
995 if ( $this->
getTitle()->isSpecial(
'RunJobs' ) ) {
997 } elseif ( $jobRunRate <= 0 ||
wfReadOnly() ) {
1001 if ( $jobRunRate < 1 ) {
1002 $max = mt_getrandmax();
1003 if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
1008 $n = intval( $jobRunRate );
1011 $logger = LoggerFactory::getInstance(
'runJobs' );
1014 if ( $this->config->get(
'RunJobsAsync' ) ) {
1017 if ( !$invokedWithSuccess ) {
1019 $logger->warning(
"Jobs switched to blocking; Special:RunJobs disabled" );
1037 $old = $trxProfiler->setSilenced(
true );
1039 $runner =
new JobRunner( $runJobsLogger );
1040 $runner->run( [
'maxJobs' => $n ] );
1042 $trxProfiler->setSilenced( $old );
1058 $query = [
'title' =>
'Special:RunJobs',
1059 'tasks' =>
'jobs',
'maxjobs' => $n,
'sigexpiry' => time() + 5 ];
1061 $query, $this->config->get(
'SecretKey' ) );
1063 $errno = $errstr =
null;
1064 $info =
wfParseUrl( $this->config->get(
'CanonicalServer' ) );
1065 $host = $info ? $info[
'host'] :
null;
1067 if ( isset( $info[
'scheme'] ) && $info[
'scheme'] ==
'https' ) {
1068 $host =
"tls://" . $host;
1071 if ( isset( $info[
'port'] ) ) {
1072 $port = $info[
'port'];
1075 Wikimedia\suppressWarnings();
1076 $sock = $host ? fsockopen(
1084 Wikimedia\restoreWarnings();
1086 $invokedWithSuccess =
true;
1089 getPage(
'RunJobs' );
1090 $url = $special->getPageTitle()->getCanonicalURL( $query );
1092 "POST $url HTTP/1.1\r\n" .
1093 "Host: {$info['host']}\r\n" .
1094 "Connection: Close\r\n" .
1095 "Content-Length: 0\r\n\r\n"
1098 $runJobsLogger->info(
"Running $n job(s) via '$url'" );
1101 stream_set_timeout( $sock, 2 );
1102 $bytes = fwrite( $sock, $req );
1103 if ( $bytes !== strlen( $req ) ) {
1104 $invokedWithSuccess =
false;
1105 $runJobsLogger->error(
"Failed to start cron API (socket write error)" );
1109 $start = microtime(
true );
1111 $sec = microtime(
true ) - $start;
1112 if ( !preg_match(
'#^HTTP/\d\.\d 202 #',
$status ) ) {
1113 $invokedWithSuccess =
false;
1114 $runJobsLogger->error(
"Failed to start cron API: received '$status' ($sec)" );
1119 $invokedWithSuccess =
false;
1120 $runJobsLogger->error(
"Failed to start cron API (socket error $errno): $errstr" );
1123 return $invokedWithSuccess;