MediaWiki REL1_39
MediaWiki.php
Go to the documentation of this file.
1<?php
23use Liuggio\StatsdClient\Sender\SocketSender;
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
29use Psr\Log\LoggerInterface;
30use Wikimedia\AtEase\AtEase;
33use Wikimedia\ScopedCallback;
34
38class MediaWiki {
39 use ProtectedHookAccessorTrait;
40
42 private $context;
44 private $config;
45
47 private $action;
49 private $postSendStrategy;
50
52 private const DEFER_FASTCGI_FINISH_REQUEST = 1;
54 private const DEFER_SET_LENGTH_AND_FLUSH = 2;
56 private const DEFER_CLI_MODE = 3;
57
61 public function __construct( IContextSource $context = null ) {
62 $this->context = $context ?: RequestContext::getMain();
63 $this->config = $this->context->getConfig();
64
65 if ( $GLOBALS['wgCommandLineMode'] ) {
66 $this->postSendStrategy = self::DEFER_CLI_MODE;
67 } elseif ( function_exists( 'fastcgi_finish_request' ) ) {
68 $this->postSendStrategy = self::DEFER_FASTCGI_FINISH_REQUEST;
69 } else {
70 $this->postSendStrategy = self::DEFER_SET_LENGTH_AND_FLUSH;
71 }
72 }
73
80 private function parseTitle() {
81 $request = $this->context->getRequest();
82 $curid = $request->getInt( 'curid' );
83 $title = $request->getText( 'title' );
84
85 if ( $curid ) {
86 // URLs like this are generated by RC, because rc_title isn't always accurate
87 $ret = Title::newFromID( $curid );
88 } else {
89 $ret = Title::newFromURL( $title );
90 if ( $ret !== null ) {
91 // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA
92 // in wikitext links to tell Parser to make a direct file link
93 if ( $ret->getNamespace() === NS_MEDIA ) {
94 $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() );
95 }
96 // Check variant links so that interwiki links don't have to worry
97 // about the possible different language variants
99 $languageConverter = $services
100 ->getLanguageConverterFactory()
101 ->getLanguageConverter( $services->getContentLanguage() );
102 if ( $languageConverter->hasVariants() && !$ret->exists() ) {
103 $languageConverter->findVariantLink( $title, $ret );
104 }
105 }
106 }
107
108 // If title is not provided, always allow oldid and diff to set the title.
109 // If title is provided, allow oldid and diff to override the title, unless
110 // we are talking about a special page which might use these parameters for
111 // other purposes.
112 if ( $ret === null || !$ret->isSpecialPage() ) {
113 // We can have urls with just ?diff=,?oldid= or even just ?diff=
114 $oldid = $request->getInt( 'oldid' );
115 $oldid = $oldid ?: $request->getInt( 'diff' );
116 // Allow oldid to override a changed or missing title
117 if ( $oldid ) {
118 $revRecord = MediaWikiServices::getInstance()
119 ->getRevisionLookup()
120 ->getRevisionById( $oldid );
121 if ( $revRecord ) {
123 $revRecord->getPageAsLinkTarget()
124 );
125 }
126 }
127 }
128
129 if ( $ret === null && $request->getCheck( 'search' ) ) {
130 // Compatibility with old search URLs which didn't use Special:Search
131 // Just check for presence here, so blank requests still
132 // show the search page when using ugly URLs (T10054).
133 $ret = SpecialPage::getTitleFor( 'Search' );
134 }
135
136 // Use the main page as default title if nothing else has been provided
137 if ( $ret === null
138 && strval( $title ) === ''
139 && !$request->getCheck( 'curid' )
140 && $request->getRawVal( 'action' ) !== 'delete'
141 ) {
142 $ret = Title::newMainPage();
143 }
144
145 if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) {
146 // If we get here, we definitely don't have a valid title; throw an exception.
147 // Try to get detailed invalid title exception first, fall back to MalformedTitleException.
149 throw new MalformedTitleException( 'badtitletext', $title );
150 }
151
152 return $ret;
153 }
154
159 public function getTitle() {
160 if ( !$this->context->hasTitle() ) {
161 try {
162 $this->context->setTitle( $this->parseTitle() );
163 } catch ( MalformedTitleException $ex ) {
164 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
165 }
166 }
167 return $this->context->getTitle();
168 }
169
175 public function getAction(): string {
176 if ( $this->action === null ) {
177 $this->action = $this->context->getActionName();
178 }
179
180 return $this->action;
181 }
182
195 private function performRequest() {
196 global $wgTitle;
197
198 $request = $this->context->getRequest();
199 $output = $this->context->getOutput();
200
201 if ( $request->getRawVal( 'printable' ) === 'yes' ) {
202 $output->setPrintable();
203 }
204
205 $user = $this->context->getUser();
206 $title = $this->context->getTitle();
207 $requestTitle = $title;
208 $this->getHookRunner()->onBeforeInitialize( $title, null, $output, $user, $request, $this );
209
210 // Invalid titles. T23776: The interwikis must redirect even if the page name is empty.
211 if ( $title === null || ( $title->getDBkey() == '' && !$title->isExternal() )
212 || $title->isSpecial( 'Badtitle' )
213 ) {
214 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
215 try {
216 $this->parseTitle();
217 } catch ( MalformedTitleException $ex ) {
218 throw new BadTitleError( $ex );
219 }
220 throw new BadTitleError();
221 }
222
223 // Check user's permissions to read this page.
224 // We have to check here to catch special pages etc.
225 // We will check again in Article::view().
226 $permissionStatus = PermissionStatus::newEmpty();
227 if ( !$this->context->getAuthority()->authorizeRead( 'read', $title, $permissionStatus ) ) {
228 // T34276: allowing the skin to generate output with $wgTitle or
229 // $this->context->title set to the input title would allow anonymous users to
230 // determine whether a page exists, potentially leaking private data. In fact, the
231 // curid and oldid request parameters would allow page titles to be enumerated even
232 // when they are not guessable. So we reset the title to Special:Badtitle before the
233 // permissions error is displayed.
234
235 // The skin mostly uses $this->context->getTitle() these days, but some extensions
236 // still use $wgTitle.
237 $badTitle = SpecialPage::getTitleFor( 'Badtitle' );
238 $this->context->setTitle( $badTitle );
239 $wgTitle = $badTitle;
240
241 throw new PermissionsError( 'read', $permissionStatus );
242 }
243
244 // Interwiki redirects
245 if ( $title->isExternal() ) {
246 $rdfrom = $request->getVal( 'rdfrom' );
247 if ( $rdfrom ) {
248 $url = $title->getFullURL( [ 'rdfrom' => $rdfrom ] );
249 } else {
250 $query = $request->getValues();
251 unset( $query['title'] );
252 $url = $title->getFullURL( $query );
253 }
254 // Check for a redirect loop
255 if ( !preg_match( '/^' . preg_quote( $this->config->get( MainConfigNames::Server ), '/' ) . '/', $url )
256 && $title->isLocal()
257 ) {
258 // 301 so google et al report the target as the actual url.
259 $output->redirect( $url, 301 );
260 } else {
261 $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
262 try {
263 $this->parseTitle();
264 } catch ( MalformedTitleException $ex ) {
265 throw new BadTitleError( $ex );
266 }
267 throw new BadTitleError();
268 }
269 // Handle any other redirects.
270 // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
271 } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
272 // Prevent information leak via Special:MyPage et al (T109724)
273 $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
274 if ( $title->isSpecialPage() ) {
275 $specialPage = $spFactory->getPage( $title->getDBkey() );
276 if ( $specialPage instanceof RedirectSpecialPage ) {
277 $specialPage->setContext( $this->context );
278 if ( $this->config->get( MainConfigNames::HideIdentifiableRedirects )
279 && $specialPage->personallyIdentifiableTarget()
280 ) {
281 [ , $subpage ] = $spFactory->resolveAlias( $title->getDBkey() );
282 $target = $specialPage->getRedirect( $subpage );
283 // Target can also be true. We let that case fall through to normal processing.
284 if ( $target instanceof Title ) {
285 if ( $target->isExternal() ) {
286 // Handle interwiki redirects
287 $target = SpecialPage::getTitleFor(
288 'GoToInterwiki',
289 'force/' . $target->getPrefixedDBkey()
290 );
291 }
292
293 $query = $specialPage->getRedirectQuery( $subpage ) ?: [];
294 $derivateRequest = new DerivativeRequest( $request, $query );
295 $derivateRequest->setRequestURL( $request->getRequestURL() );
296 $this->context->setRequest( $derivateRequest );
297 // Do not varnish cache these. May vary even for anons
298 $output->lowerCdnMaxage( 0 );
299 $this->context->setTitle( $target );
300 $wgTitle = $target;
301 // Reset action type cache. (Special pages have only view)
302 $this->action = null;
303 $title = $target;
304 $output->addJsConfigVars( [
305 'wgInternalRedirectTargetUrl' => $target->getLinkURL( $query ),
306 ] );
307 $output->addModules( 'mediawiki.action.view.redirect' );
308 }
309 }
310 }
311 }
312
313 // Special pages ($title may have changed since if statement above)
314 if ( $title->isSpecialPage() ) {
315 // Actions that need to be made when we have a special pages
316 $spFactory->executePath( $title, $this->context );
317 } else {
318 // ...otherwise treat it as an article view. The article
319 // may still be a wikipage redirect to another article or URL.
320 $article = $this->initializeArticle();
321 if ( is_object( $article ) ) {
322 $this->performAction( $article, $requestTitle );
323 } elseif ( is_string( $article ) ) {
324 $output->redirect( $article );
325 } else {
326 throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
327 . " returned neither an object nor a URL" );
328 }
329 }
330 $output->considerCacheSettingsFinal();
331 }
332 }
333
356 private function tryNormaliseRedirect( Title $title ) {
357 $request = $this->context->getRequest();
358 $output = $this->context->getOutput();
359
360 if ( $request->getRawVal( 'action', 'view' ) != 'view'
361 || $request->wasPosted()
362 || ( $request->getCheck( 'title' )
363 && $title->getPrefixedDBkey() == $request->getText( 'title' ) )
364 || count( $request->getValueNames( [ 'action', 'title' ] ) )
365 || !$this->getHookRunner()->onTestCanonicalRedirect( $request, $title, $output )
366 ) {
367 return false;
368 }
369
370 if ( $this->config->get( MainConfigNames::MainPageIsDomainRoot ) && $request->getRequestURL() === '/' ) {
371 return false;
372 }
373
374 if ( $title->isSpecialPage() ) {
375 [ $name, $subpage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()->
376 resolveAlias( $title->getDBkey() );
377 if ( $name ) {
378 $title = SpecialPage::getTitleFor( $name, $subpage );
379 }
380 }
381 // Redirect to canonical url, make it a 301 to allow caching
382 $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
383 if ( $targetUrl == $request->getFullRequestURL() ) {
384 $message = "Redirect loop detected!\n\n" .
385 "This means the wiki got confused about what page was " .
386 "requested; this sometimes happens when moving a wiki " .
387 "to a new server or changing the server configuration.\n\n";
388
389 if ( $this->config->get( MainConfigNames::UsePathInfo ) ) {
390 $message .= "The wiki is trying to interpret the page " .
391 "title from the URL path portion (PATH_INFO), which " .
392 "sometimes fails depending on the web server. Try " .
393 "setting \"\$wgUsePathInfo = false;\" in your " .
394 "LocalSettings.php, or check that \$wgArticlePath " .
395 "is correct.";
396 } else {
397 $message .= "Your web server was detected as possibly not " .
398 "supporting URL path components (PATH_INFO) correctly; " .
399 "check your LocalSettings.php for a customized " .
400 "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
401 "to true.";
402 }
403 throw new HttpError( 500, $message );
404 }
405 $output->setCdnMaxage( 1200 );
406 $output->redirect( $targetUrl, '301' );
407 return true;
408 }
409
416 private function initializeArticle() {
417 $title = $this->context->getTitle();
418 $services = MediaWikiServices::getInstance();
419 if ( $this->context->canUseWikiPage() ) {
420 // Reuse the WikiPage instance from context, as it may already have been initialized
421 // by an earlier this->getAction() call.
422 $page = $this->context->getWikiPage();
423 } else {
424 // This case should not happen, but just in case.
425 // @TODO: remove this or use an exception
426 $page = $services->getWikiPageFactory()->newFromTitle( $title );
427 $this->context->setWikiPage( $page );
428 wfWarn( "RequestContext::canUseWikiPage() returned false" );
429 }
430
431 // Make GUI wrapper for the WikiPage
432 $article = Article::newFromWikiPage( $page, $this->context );
433
434 // Skip some unnecessary code if the content model doesn't support redirects
435 // Use the page content model rather than invoking Title::getContentModel()
436 // to avoid querying page data twice (T206498)
437 if ( !$page->getContentHandler()->supportsRedirects() ) {
438 return $article;
439 }
440
441 $request = $this->context->getRequest();
442
443 // Namespace might change when using redirects
444 // Check for redirects ...
445 $action = $request->getRawVal( 'action', 'view' );
446 $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null;
447 if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
448 && !$request->getCheck( 'oldid' ) // ... and are not old revisions
449 && !$request->getCheck( 'diff' ) // ... and not when showing diff
450 && $request->getRawVal( 'redirect' ) !== 'no' // ... unless explicitly told not to
451 // ... and the article is not a non-redirect image page with associated file
452 && !( is_object( $file ) && $file->exists() && !$file->getRedirected() )
453 ) {
454 // Give extensions a change to ignore/handle redirects as needed
455 $ignoreRedirect = $target = false;
456
457 $this->getHookRunner()->onInitializeArticleMaybeRedirect( $title, $request,
458 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
459 $ignoreRedirect, $target, $article );
460 $page = $article->getPage(); // reflect any hook changes
461
462 // Follow redirects only for... redirects.
463 // If $target is set, then a hook wanted to redirect.
464 if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
465 // Is the target already set by an extension?
466 $target = $target ?: $page->followRedirect();
467 if ( is_string( $target ) && !$this->config->get( MainConfigNames::DisableHardRedirects ) ) {
468 // we'll need to redirect
469 return $target;
470 }
471 if ( is_object( $target ) ) {
472 // Rewrite environment to redirected article
473 $rpage = $services->getWikiPageFactory()->newFromTitle( $target );
474 $rpage->loadPageData();
475 if ( $rpage->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
476 $rarticle = Article::newFromWikiPage( $rpage, $this->context );
477 $rarticle->setRedirectedFrom( $title );
478
479 $article = $rarticle;
480 $this->context->setTitle( $target );
481 $this->context->setWikiPage( $article->getPage() );
482 }
483 }
484 } else {
485 // Article may have been changed by hook
486 $this->context->setTitle( $article->getTitle() );
487 $this->context->setWikiPage( $article->getPage() );
488 }
489 }
490
491 return $article;
492 }
493
500 private function performAction( Article $article, Title $requestTitle ) {
501 $request = $this->context->getRequest();
502 $output = $this->context->getOutput();
503 $title = $this->context->getTitle();
504 $user = $this->context->getUser();
505
506 if ( !$this->getHookRunner()->onMediaWikiPerformAction(
507 $output, $article, $title, $user, $request, $this )
508 ) {
509 return;
510 }
511
512 $t = microtime( true );
513 $actionName = $this->getAction();
514 $services = MediaWikiServices::getInstance();
515 $action = $services->getActionFactory()->getAction( $actionName, $article, $this->context );
516
517 if ( $action instanceof Action ) {
518 // Check read permissions
519 if ( $action->needsReadRights() && !$user->isAllowed( 'read' ) ) {
520 throw new PermissionsError( 'read' );
521 }
522
523 // Narrow DB query expectations for this HTTP request
524 if ( $request->wasPosted() && !$action->doesWrites() ) {
525 $trxProfiler = Profiler::instance()->getTransactionProfiler();
526 $trxLimits = $this->config->get( MainConfigNames::TrxProfilerLimits );
527 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
528 $request->markAsSafeRequest();
529 }
530
531 // Let CDN cache things if we can purge them.
532 // Also unconditionally cache page views.
533 if ( $this->config->get( MainConfigNames::UseCdn ) ) {
534 $htmlCacheUpdater = $services->getHtmlCacheUpdater();
535 if ( $request->matchUrlForCdn( $htmlCacheUpdater->getUrls( $requestTitle ) ) ) {
536 $output->setCdnMaxage( $this->config->get( MainConfigNames::CdnMaxAge ) );
537 } elseif ( $action instanceof ViewAction ) {
538 $output->setCdnMaxage( 3600 );
539 }
540 }
541
542 $action->show();
543
544 $runTime = microtime( true ) - $t;
545 $services->getStatsdDataFactory()->timing(
546 'action.' . strtr( $actionName, '.', '_' ) . '.executeTiming',
547 1000 * $runTime
548 );
549 return;
550 }
551
552 // If we've not found out which action it is by now, it's unknown
553 $output->setStatusCode( 404 );
554 $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
555 }
556
560 public function run() {
561 try {
562 $this->main();
563 } catch ( Exception $e ) {
564 $context = $this->context;
565 $action = $context->getRequest()->getRawVal( 'action', 'view' );
566 if (
567 $e instanceof DBConnectionError &&
568 $context->hasTitle() &&
569 $context->getTitle()->canExist() &&
570 in_array( $action, [ 'view', 'history' ], true ) &&
572 ) {
573 // Try to use any (even stale) file during outages...
574 $cache = new HTMLFileCache( $context->getTitle(), $action );
575 if ( $cache->isCached() ) {
576 $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE );
577 print MWExceptionRenderer::getHTML( $e );
578 exit;
579 }
580 }
581 MWExceptionHandler::handleException( $e, MWExceptionHandler::CAUGHT_BY_ENTRYPOINT );
582 } catch ( Throwable $e ) {
583 // Type errors and such: at least handle it now and clean up the LBFactory state
584 MWExceptionHandler::handleException( $e, MWExceptionHandler::CAUGHT_BY_ENTRYPOINT );
585 }
586
587 $this->doPostOutputShutdown();
588 }
589
593 private function schedulePostSendJobs() {
594 $jobRunRate = $this->config->get( MainConfigNames::JobRunRate );
595 if (
596 // Recursion guard
597 $this->getTitle()->isSpecial( 'RunJobs' ) ||
598 // Short circuit if there is nothing to do
599 $jobRunRate <= 0 ||
600 MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ||
601 // Avoid blocking the client on stock apache; see doPostOutputShutdown()
602 $this->context->getRequest()->getMethod() === 'HEAD' ||
603 $this->context->getRequest()->getHeader( 'If-Modified-Since' )
604 ) {
605 return;
606 }
607
608 if ( $jobRunRate < 1 ) {
609 $max = mt_getrandmax();
610 if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
611 return; // the higher the job run rate, the less likely we return here
612 }
613 $n = 1;
614 } else {
615 $n = intval( $jobRunRate );
616 }
617
618 // Note that DeferredUpdates will catch and log any errors (T88312)
620 $logger = LoggerFactory::getInstance( 'runJobs' );
621 if ( $this->config->get( MainConfigNames::RunJobsAsync ) ) {
622 // Send an HTTP request to the job RPC entry point if possible
623 $invokedWithSuccess = $this->triggerAsyncJobs( $n, $logger );
624 if ( !$invokedWithSuccess ) {
625 // Fall back to blocking on running the job(s)
626 $logger->warning( "Jobs switched to blocking; Special:RunJobs disabled" );
627 $this->triggerSyncJobs( $n );
628 }
629 } else {
630 $this->triggerSyncJobs( $n );
631 }
632 }, __METHOD__ ) );
633 }
634
639 private function doPreOutputCommit() {
640 self::preOutputCommit( $this->context );
641 }
642
654 public static function preOutputCommit(
655 IContextSource $context, $postCommitWork = null
656 ) {
657 $config = $context->getConfig();
658 $request = $context->getRequest();
659 $output = $context->getOutput();
660 $services = MediaWikiServices::getInstance();
661 $lbFactory = $services->getDBLoadBalancerFactory();
662
663 // Try to make sure that all RDBMs, session, and other storage updates complete
664 ignore_user_abort( true );
665
666 // Commit all RDBMs changes from the main transaction round
667 $lbFactory->commitPrimaryChanges(
668 __METHOD__,
669 // Abort if any transaction was too big
670 [ 'maxWriteDuration' => $config->get( MainConfigNames::MaxUserDBWriteDuration ) ]
671 );
672 wfDebug( __METHOD__ . ': primary transaction round committed' );
673
674 // Run updates that need to block the client or affect output (this is the last chance)
675 DeferredUpdates::doUpdates(
676 null,
677 $config->get( MainConfigNames::ForceDeferredUpdatesPreSend )
678 ? DeferredUpdates::ALL
679 : DeferredUpdates::PRESEND
680 );
681
682 wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
683
684 // Persist the session to avoid race conditions on subsequent requests by the client
685 $request->getSession()->save(); // T214471
686 wfDebug( __METHOD__ . ': session changes committed' );
687
688 // Subsequent requests by the client should see the DB replication positions written
689 // during the shutdown() call below, even if the position store itself has asynchronous
690 // replication. Setting the cpPosIndex cookie is normally enough. However, this might not
691 // work for cross-domain redirects to foreign wikis, so set the ?cpPoxIndex in that case.
692 $isCrossWikiRedirect = (
693 $output->getRedirect() &&
694 $lbFactory->hasOrMadeRecentPrimaryChanges( INF ) &&
695 self::getUrlDomainDistance( $output->getRedirect() ) === 'remote'
696 );
697
698 // Persist replication positions for DBs modified by this request (at this point).
699 // These help provide "session consistency" for the client on their next requests.
700 $cpIndex = null;
701 $cpClientId = null;
702 $lbFactory->shutdown(
703 $lbFactory::SHUTDOWN_NORMAL,
704 null,
705 $cpIndex,
706 $cpClientId
707 );
708 $now = time();
709
710 $allowHeaders = !( $output->isDisabled() || headers_sent() );
711
712 if ( $cpIndex > 0 ) {
713 if ( $allowHeaders ) {
714 $expires = $now + ChronologyProtector::POSITION_COOKIE_TTL;
715 $options = [ 'prefix' => '' ];
716 $value = $lbFactory::makeCookieValueFromCPIndex( $cpIndex, $now, $cpClientId );
717 $request->response()->setCookie( 'cpPosIndex', $value, $expires, $options );
718 }
719
720 if ( $isCrossWikiRedirect ) {
721 if ( $output->getRedirect() ) {
722 $safeUrl = $lbFactory->appendShutdownCPIndexAsQuery(
723 $output->getRedirect(),
724 $cpIndex
725 );
726 $output->redirect( $safeUrl );
727 } else {
728 MWExceptionHandler::logException(
729 new LogicException( "No redirect; cannot append cpPosIndex parameter." ),
730 MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
731 );
732 }
733 }
734 }
735
736 if ( $allowHeaders ) {
737 // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that
738 // handles this POST request (e.g. the "primary" data center). Also have the user
739 // briefly bypass CDN so ChronologyProtector works for cacheable URLs.
740 if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentPrimaryChanges() ) {
741 $expires = $now + max(
742 ChronologyProtector::POSITION_COOKIE_TTL,
743 $config->get( MainConfigNames::DataCenterUpdateStickTTL )
744 );
745 $options = [ 'prefix' => '' ];
746 $request->response()->setCookie( 'UseDC', 'master', $expires, $options );
747 $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
748 }
749
750 // Avoid letting a few seconds of replica DB lag cause a month of stale data.
751 // This logic is also intimately related to the value of $wgCdnReboundPurgeDelay.
752 if ( $lbFactory->laggedReplicaUsed() ) {
753 $maxAge = $config->get( MainConfigNames::CdnMaxageLagged );
754 $output->lowerCdnMaxage( $maxAge );
755 $request->response()->header( "X-Database-Lagged: true" );
756 wfDebugLog( 'replication',
757 "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
758 }
759
760 // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
761 if ( $services->getMessageCache()->isDisabled() ) {
762 $maxAge = $config->get( MainConfigNames::CdnMaxageSubstitute );
763 $output->lowerCdnMaxage( $maxAge );
764 $request->response()->header( "X-Response-Substitute: true" );
765 }
766
767 if ( !$output->couldBePublicCached() || $output->haveCacheVaryCookies() ) {
768 // Autoblocks: If this user is autoblocked (and the cookie block feature is enabled
769 // for autoblocks), then set a cookie to track this block.
770 // This has to be done on all logged-in page loads (not just upon saving edits),
771 // because an autoblocked editor might not edit again from the same IP address.
772 //
773 // IP blocks: For anons, if their IP is blocked (and cookie block feature is enabled
774 // for IP blocks), we also want to set the cookie whenever it is safe to do.
775 // Basically from any url that are definitely not publicly cacheable (like viewing
776 // EditPage), or when the HTTP response is personalised for other reasons (e.g. viewing
777 // articles within the same browsing session after making an edit).
778 $user = $context->getUser();
779 $services->getBlockManager()
780 ->trackBlockWithCookie( $user, $request->response() );
781 }
782 }
783 }
784
789 private static function getUrlDomainDistance( $url ) {
790 $clusterWiki = WikiMap::getWikiFromUrl( $url );
791 if ( WikiMap::isCurrentWikiId( $clusterWiki ) ) {
792 return 'local'; // the current wiki
793 }
794 if ( $clusterWiki !== false ) {
795 return 'remote'; // another wiki in this cluster/farm
796 }
797
798 return 'external';
799 }
800
810 public function doPostOutputShutdown() {
811 // Record backend request timing
812 $timing = $this->context->getTiming();
813 $timing->mark( 'requestShutdown' );
814
815 // Defer everything else if possible...
816 if ( $this->postSendStrategy === self::DEFER_FASTCGI_FINISH_REQUEST ) {
817 // Flush the output to the client, continue processing, and avoid further output
818 fastcgi_finish_request();
819 } elseif ( $this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH ) {
820 // Flush the output to the client, continue processing, and avoid further output
821 if ( ob_get_level() ) {
822 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
823 @ob_end_flush();
824 }
825 // Flush the web server output buffer to the client/proxy if possible
826 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
827 @flush();
828 }
829
830 // Since the headers and output where already flushed, disable WebResponse setters
831 // during post-send processing to warnings and unexpected behavior (T191537)
832 WebResponse::disableForPostSend();
833 // Run post-send updates while preventing further output...
834 ob_start( static function () {
835 return ''; // do not output uncaught exceptions
836 } );
837 try {
838 $this->restInPeace();
839 } catch ( Throwable $e ) {
840 MWExceptionHandler::rollbackPrimaryChangesAndLog(
841 $e,
842 MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
843 );
844 }
845 $length = ob_get_length();
846 if ( $length > 0 ) {
847 trigger_error( __METHOD__ . ": suppressed $length byte(s)", E_USER_NOTICE );
848 }
849 ob_end_clean();
850 }
851
855 private function main() {
856 global $wgTitle;
857
858 // Get title from request parameters,
859 // is set on the fly by parseTitle the first time.
860 $title = $this->getTitle();
861 $wgTitle = $title;
862
863 $request = $this->context->getRequest();
864 // Set DB query expectations for this HTTP request
865 $trxLimits = $this->config->get( MainConfigNames::TrxProfilerLimits );
866 $trxProfiler = Profiler::instance()->getTransactionProfiler();
867 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
868 if ( $request->hasSafeMethod() ) {
869 $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
870 } else {
871 $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
872 }
873
874 if ( $this->maybeDoHttpsRedirect() ) {
875 return;
876 }
877
878 $output = $this->context->getOutput();
879 if ( $title->canExist() && HTMLFileCache::useFileCache( $this->context ) ) {
880 // getAction() may trigger DB queries, so avoid eagerly initializing it if possible.
881 // This reduces the cost of requests that exit early due to tryNormaliseRedirect()
882 // or a MediaWikiPerformAction / BeforeInitialize hook handler.
883 $action = $this->getAction();
884 // Try low-level file cache hit
885 $cache = new HTMLFileCache( $title, $action );
886 if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
887 // Check incoming headers to see if client has this cached
888 $timestamp = $cache->cacheTimestamp();
889 if ( !$output->checkLastModified( $timestamp ) ) {
890 $cache->loadFromFileCache( $this->context );
891 }
892 // Do any stats increment/watchlist stuff, assuming user is viewing the
893 // latest revision (which should always be the case for file cache)
894 $this->context->getWikiPage()->doViewUpdates( $this->context->getAuthority() );
895 // Tell OutputPage that output is taken care of
896 $output->disable();
897
898 return;
899 }
900 }
901
902 try {
903 // Actually do the work of the request and build up any output
904 $this->performRequest();
905 } catch ( ErrorPageError $e ) {
906 // TODO: Should ErrorPageError::report accept a OutputPage parameter?
907 $e->report( ErrorPageError::STAGE_OUTPUT );
908 $output->considerCacheSettingsFinal();
909 // T64091: while exceptions are convenient to bubble up GUI errors,
910 // they are not internal application faults. As with normal requests, this
911 // should commit, print the output, do deferred updates, jobs, and profiling.
912 }
913
914 // Commit any changes in the current transaction round so that:
915 // a) the transaction is not rolled back after success output was already sent
916 // b) error output is not jumbled together with success output in the response
917 $this->doPreOutputCommit();
918 // If needed, push a deferred update to run jobs after the output is send
919 $this->schedulePostSendJobs();
920 // Ask OutputPage/Skin to render the page output
921 // If no exceptions occurred then send the output since it is safe now
922 $this->outputResponsePayload( $output->output( true ) );
923 }
924
931 private function shouldDoHttpRedirect() {
932 $request = $this->context->getRequest();
933
934 // Don't redirect if we're already on HTTPS
935 if ( $request->getProtocol() !== 'http' ) {
936 return false;
937 }
938
939 $force = $this->config->get( MainConfigNames::ForceHTTPS );
940
941 // Don't redirect if $wgServer is explicitly HTTP. We test for this here
942 // by checking whether wfExpandUrl() is able to force HTTPS.
943 if ( !preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) ) {
944 if ( $force ) {
945 throw new RuntimeException( '$wgForceHTTPS is true but the server is not HTTPS' );
946 }
947 return false;
948 }
949
950 // Configured $wgForceHTTPS overrides the remaining conditions
951 if ( $force ) {
952 return true;
953 }
954
955 // Check if HTTPS is required by the session or user preferences
956 return $request->getSession()->shouldForceHTTPS() ||
957 // Check the cookie manually, for paranoia
958 $request->getCookie( 'forceHTTPS', '' ) ||
959 $this->context->getUser()->requiresHTTPS();
960 }
961
971 private function maybeDoHttpsRedirect() {
972 if ( !$this->shouldDoHttpRedirect() ) {
973 return false;
974 }
975
976 $request = $this->context->getRequest();
977 $oldUrl = $request->getFullRequestURL();
978 $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
979
980 if ( $request->wasPosted() ) {
981 // This is weird and we'd hope it almost never happens. This
982 // means that a POST came in via HTTP and policy requires us
983 // redirecting to HTTPS. It's likely such a request is going
984 // to fail due to post data being lost, but let's try anyway
985 // and just log the instance.
986
987 // @todo FIXME: See if we could issue a 307 or 308 here, need
988 // to see how clients (automated & browser) behave when we do
989 wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
990 }
991 // Setup dummy Title, otherwise OutputPage::redirect will fail
992 $title = Title::newFromText( 'REDIR', NS_MAIN );
993 $this->context->setTitle( $title );
994 // Since we only do this redir to change proto, always send a vary header
995 $output = $this->context->getOutput();
996 $output->addVaryHeader( 'X-Forwarded-Proto' );
997 $output->redirect( $redirUrl );
998 $output->output();
999
1000 return true;
1001 }
1002
1012 private function outputResponsePayload( $content ) {
1013 // Append any visible profiling data in a manner appropriate for the Content-Type
1014 ob_start();
1015 try {
1016 Profiler::instance()->logDataPageOutputOnly();
1017 } finally {
1018 $content .= ob_get_clean();
1019 }
1020
1021 // By default, usually one output buffer is active now, either the internal PHP buffer
1022 // started by "output_buffering" in php.ini or the buffer started by MW_SETUP_CALLBACK.
1023 // The MW_SETUP_CALLBACK buffer has an unlimited chunk size, while the internal PHP
1024 // buffer only has an unlimited chunk size if output_buffering="On". If the buffer was
1025 // filled up to the chunk size with printed data, then HTTP headers will have already
1026 // been sent. Also, if the entry point had to stream content to the client, then HTTP
1027 // headers will have already been sent as well, regardless of chunk size.
1028
1029 // Disable mod_deflate compression since it interferes with the output buffer set
1030 // by MW_SETUP_CALLBACK and can also cause the client to wait on deferred updates
1031 if ( function_exists( 'apache_setenv' ) ) {
1032 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1033 @apache_setenv( 'no-gzip', '1' );
1034 }
1035
1036 if (
1037 // "Content-Length" is used to prevent clients from waiting on deferred updates
1038 $this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH &&
1039 // The HTTP response code clearly allows for a meaningful body
1040 in_array( http_response_code(), [ 200, 404 ], true ) &&
1041 // The queue of (post-send) deferred updates is non-empty
1042 DeferredUpdates::pendingUpdatesCount() &&
1043 // Any buffered output is not spread out across multiple output buffers
1044 ob_get_level() <= 1 &&
1045 // It is not too late to set additional HTTP headers
1046 !headers_sent()
1047 ) {
1048 $response = $this->context->getRequest()->response();
1049
1050 $obStatus = ob_get_status();
1051 if ( !isset( $obStatus['name'] ) ) {
1052 // No output buffer is active
1053 $response->header( 'Content-Length: ' . strlen( $content ) );
1054 } elseif ( $obStatus['name'] === 'default output handler' ) {
1055 // Internal PHP "output_buffering" output buffer (note that the internal PHP
1056 // "zlib.output_compression" output buffer is named "zlib output compression")
1057 $response->header( 'Content-Length: ' . ( ob_get_length() + strlen( $content ) ) );
1058 }
1059
1060 // The MW_SETUP_CALLBACK output buffer ("MediaWiki\OutputHandler::handle") sets
1061 // "Content-Length" where applicable. Other output buffer types might not set this
1062 // header, and since they might mangle or compress the payload, it is not possible
1063 // to determine the final payload size here.
1064
1065 // Tell the client to immediately end the connection as soon as the response payload
1066 // has been read (informed by any "Content-Length" header). This prevents the client
1067 // from waiting on deferred updates.
1068 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection
1069 if ( ( $_SERVER['SERVER_PROTOCOL'] ?? '' ) === 'HTTP/1.1' ) {
1070 $response->header( 'Connection: close' );
1071 }
1072 }
1073
1074 // Print the content *after* adjusting HTTP headers and disabling mod_deflate since
1075 // calling "print" will send the output to the client if there is no output buffer or
1076 // if the output buffer chunk size is reached
1078 }
1079
1083 public function restInPeace() {
1084 // Either all DB and deferred updates should happen or none.
1085 // The latter should not be cancelled due to client disconnect.
1086 ignore_user_abort( true );
1087
1088 $services = MediaWikiServices::getInstance();
1089 $lbFactory = $services->getDBLoadBalancerFactory();
1090 // Assure deferred updates are not in the main transaction
1091 $lbFactory->commitPrimaryChanges( __METHOD__ );
1092
1093 // Loosen DB query expectations since the HTTP client is unblocked
1094 $profiler = Profiler::instance();
1095 $trxProfiler = $profiler->getTransactionProfiler();
1096 $trxProfiler->redefineExpectations(
1097 $this->context->getRequest()->hasSafeMethod()
1098 ? $this->config->get( MainConfigNames::TrxProfilerLimits )['PostSend-GET']
1099 : $this->config->get( MainConfigNames::TrxProfilerLimits )['PostSend-POST'],
1100 __METHOD__
1101 );
1102
1103 // Do any deferred jobs; preferring to run them now if a client will not wait on them
1104 DeferredUpdates::doUpdates();
1105
1106 // Handle external profiler outputs.
1107 // Any embedded profiler outputs were already processed in outputResponsePayload().
1108 $profiler->logData();
1109
1110 self::emitBufferedStatsdData(
1111 $services->getStatsdDataFactory(),
1112 $this->config
1113 );
1114
1115 // Send metrics gathered by MetricsFactory
1116 $services->getMetricsFactory()->flush();
1117
1118 // Commit and close up!
1119 $lbFactory->commitPrimaryChanges( __METHOD__ );
1120 $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT );
1121
1122 wfDebug( "Request ended normally" );
1123 }
1124
1152 public static function emitBufferedStatsdData(
1153 IBufferingStatsdDataFactory $stats, Config $config
1154 ) {
1155 if ( $config->get( MainConfigNames::StatsdServer ) && $stats->hasData() ) {
1156 try {
1157 $statsdServer = explode( ':', $config->get( MainConfigNames::StatsdServer ), 2 );
1158 $statsdHost = $statsdServer[0];
1159 $statsdPort = $statsdServer[1] ?? 8125;
1160 $statsdSender = new SocketSender( $statsdHost, $statsdPort );
1161 $statsdClient = new SamplingStatsdClient( $statsdSender, true, false );
1162 $statsdClient->setSamplingRates( $config->get( MainConfigNames::StatsdSamplingRates ) );
1163 $statsdClient->send( $stats->getData() );
1164 } catch ( Exception $e ) {
1165 MWExceptionHandler::logException( $e, MWExceptionHandler::CAUGHT_BY_ENTRYPOINT );
1166 }
1167 }
1168 // empty buffer for the next round
1169 $stats->clearData();
1170 }
1171
1175 private function triggerSyncJobs( $n ) {
1176 $scope = Profiler::instance()->getTransactionProfiler()->silenceForScope();
1177 $runner = MediaWikiServices::getInstance()->getJobRunner();
1178 $runner->run( [ 'maxJobs' => $n ] );
1179 ScopedCallback::consume( $scope );
1180 }
1181
1187 private function triggerAsyncJobs( $n, LoggerInterface $runJobsLogger ) {
1188 $services = MediaWikiServices::getInstance();
1189 // Do not send request if there are probably no jobs
1190 $group = $services->getJobQueueGroupFactory()->makeJobQueueGroup();
1191 if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
1192 return true;
1193 }
1194
1195 $query = [ 'title' => 'Special:RunJobs',
1196 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ];
1197 $query['signature'] = SpecialRunJobs::getQuerySignature(
1198 $query, $this->config->get( MainConfigNames::SecretKey ) );
1199
1200 $errno = $errstr = null;
1201 $info = wfParseUrl( $this->config->get( MainConfigNames::CanonicalServer ) );
1202 $host = $info ? $info['host'] : null;
1203 $port = 80;
1204 if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) {
1205 $host = "tls://" . $host;
1206 $port = 443;
1207 }
1208 if ( isset( $info['port'] ) ) {
1209 $port = $info['port'];
1210 }
1211
1212 AtEase::suppressWarnings();
1213 $sock = $host ? fsockopen(
1214 $host,
1215 $port,
1216 $errno,
1217 $errstr,
1218 // If it takes more than 100ms to connect to ourselves there is a problem...
1219 0.100
1220 ) : false;
1221 AtEase::restoreWarnings();
1222
1223 $invokedWithSuccess = true;
1224 if ( $sock ) {
1225 $special = $services->getSpecialPageFactory()->getPage( 'RunJobs' );
1226 $url = $special->getPageTitle()->getCanonicalURL( $query );
1227 $req = (
1228 "POST $url HTTP/1.1\r\n" .
1229 "Host: {$info['host']}\r\n" .
1230 "Connection: Close\r\n" .
1231 "Content-Length: 0\r\n\r\n"
1232 );
1233
1234 $runJobsLogger->info( "Running $n job(s) via '$url'" );
1235 // Send a cron API request to be performed in the background.
1236 // Give up if this takes too long to send (which should be rare).
1237 stream_set_timeout( $sock, 2 );
1238 $bytes = fwrite( $sock, $req );
1239 if ( $bytes !== strlen( $req ) ) {
1240 $invokedWithSuccess = false;
1241 $runJobsLogger->error( "Failed to start cron API (socket write error)" );
1242 } else {
1243 // Do not wait for the response (the script should handle client aborts).
1244 // Make sure that we don't close before that script reaches ignore_user_abort().
1245 $start = microtime( true );
1246 $status = fgets( $sock );
1247 $sec = microtime( true ) - $start;
1248 if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
1249 $invokedWithSuccess = false;
1250 $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" );
1251 }
1252 }
1253 fclose( $sock );
1254 } else {
1255 $invokedWithSuccess = false;
1256 $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" );
1257 }
1258
1259 return $invokedWithSuccess;
1260 }
1261}
const PROTO_HTTPS
Definition Defines.php:194
const NS_FILE
Definition Defines.php:70
const PROTO_CURRENT
Definition Defines.php:198
const NS_MAIN
Definition Defines.php:64
const NS_MEDIA
Definition Defines.php:52
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.
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.
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgTitle
Definition Setup.php:497
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Actions are things which can be done to pages (edit, delete, rollback, etc).
Definition Action.php:44
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:48
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:201
getTitle()
Get the title object of the article.
Definition Article.php:230
getPage()
Get the WikiPage object of this instance.
Definition Article.php:240
Show an error page on a badtitle.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
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:32
MediaWiki exception.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
A StatusValue for permission errors.
doPostOutputShutdown()
This function does work that can be done after the user gets the HTTP response so they don't block on...
static emitBufferedStatsdData(IBufferingStatsdDataFactory $stats, Config $config)
Send out any buffered statsd data according to sampling rules.
run()
Run the current MediaWiki instance; index.php just calls this.
getTitle()
Get the Title object that we'll be acting on, as specified in the WebRequest.
__construct(IContextSource $context=null)
Definition MediaWiki.php:61
getAction()
Returns the name of the action that will be executed.
restInPeace()
Ends this task peacefully.
static preOutputCommit(IContextSource $context, $postCommitWork=null)
This function commits all DB and session changes as needed before the client can receive a response (...
Show an error when a user tries to do something they do not have the necessary permissions for.
static instance()
Singleton.
Definition Profiler.php:69
Shortcut to construct a special page alias.
A statsd client that applies the sampling rate to the data items before sending them.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
static getQuerySignature(array $query, $secretKey)
Represents a title within MediaWiki.
Definition Title.php:49
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition Title.php:282
static newFromURL( $url)
THIS IS NOT THE FUNCTION YOU WANT.
Definition Title.php:467
static newFromTextThrow( $text, $defaultNamespace=NS_MAIN)
Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,...
Definition Title.php:405
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition Title.php:700
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:370
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition Title.php:638
Deferrable update that must run outside of any explicit LBFactory transaction round.
An action that views article content.
Special handling for representing file pages.
Provide a given client with protection against visible database lag.
$runner
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:69
Interface for configuration instances.
Definition Config.php:30
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.
getConfig()
Get the site configuration.
$cache
Definition mcc.php:33
A helper class for throttling authentication attempts.
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42