wikimedia /
mediawiki
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * Helper class for the index.php entry point. |
||
| 4 | * |
||
| 5 | * This program is free software; you can redistribute it and/or modify |
||
| 6 | * it under the terms of the GNU General Public License as published by |
||
| 7 | * the Free Software Foundation; either version 2 of the License, or |
||
| 8 | * (at your option) any later version. |
||
| 9 | * |
||
| 10 | * This program is distributed in the hope that it will be useful, |
||
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 13 | * GNU General Public License for more details. |
||
| 14 | * |
||
| 15 | * You should have received a copy of the GNU General Public License along |
||
| 16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
| 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 18 | * http://www.gnu.org/copyleft/gpl.html |
||
| 19 | * |
||
| 20 | * @file |
||
| 21 | */ |
||
| 22 | |||
| 23 | use MediaWiki\Logger\LoggerFactory; |
||
| 24 | use MediaWiki\MediaWikiServices; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * The MediaWiki class is the helper class for the index.php entry point. |
||
| 28 | */ |
||
| 29 | class MediaWiki { |
||
| 30 | /** |
||
| 31 | * @var IContextSource |
||
| 32 | */ |
||
| 33 | private $context; |
||
| 34 | |||
| 35 | /** |
||
| 36 | * @var Config |
||
| 37 | */ |
||
| 38 | private $config; |
||
| 39 | |||
| 40 | /** |
||
| 41 | * @var String Cache what action this request is |
||
| 42 | */ |
||
| 43 | private $action; |
||
| 44 | |||
| 45 | /** |
||
| 46 | * @param IContextSource|null $context |
||
| 47 | */ |
||
| 48 | public function __construct( IContextSource $context = null ) { |
||
| 49 | if ( !$context ) { |
||
| 50 | $context = RequestContext::getMain(); |
||
| 51 | } |
||
| 52 | |||
| 53 | $this->context = $context; |
||
| 54 | $this->config = $context->getConfig(); |
||
| 55 | } |
||
| 56 | |||
| 57 | /** |
||
| 58 | * Parse the request to get the Title object |
||
| 59 | * |
||
| 60 | * @throws MalformedTitleException If a title has been provided by the user, but is invalid. |
||
| 61 | * @return Title Title object to be $wgTitle |
||
| 62 | */ |
||
| 63 | private function parseTitle() { |
||
| 64 | global $wgContLang; |
||
| 65 | |||
| 66 | $request = $this->context->getRequest(); |
||
| 67 | $curid = $request->getInt( 'curid' ); |
||
| 68 | $title = $request->getVal( 'title' ); |
||
| 69 | $action = $request->getVal( 'action' ); |
||
| 70 | |||
| 71 | if ( $request->getCheck( 'search' ) ) { |
||
| 72 | // Compatibility with old search URLs which didn't use Special:Search |
||
| 73 | // Just check for presence here, so blank requests still |
||
| 74 | // show the search page when using ugly URLs (bug 8054). |
||
| 75 | $ret = SpecialPage::getTitleFor( 'Search' ); |
||
| 76 | } elseif ( $curid ) { |
||
| 77 | // URLs like this are generated by RC, because rc_title isn't always accurate |
||
| 78 | $ret = Title::newFromID( $curid ); |
||
| 79 | } else { |
||
| 80 | $ret = Title::newFromURL( $title ); |
||
| 81 | // Alias NS_MEDIA page URLs to NS_FILE...we only use NS_MEDIA |
||
| 82 | // in wikitext links to tell Parser to make a direct file link |
||
| 83 | if ( !is_null( $ret ) && $ret->getNamespace() == NS_MEDIA ) { |
||
| 84 | $ret = Title::makeTitle( NS_FILE, $ret->getDBkey() ); |
||
| 85 | } |
||
| 86 | // Check variant links so that interwiki links don't have to worry |
||
| 87 | // about the possible different language variants |
||
| 88 | if ( count( $wgContLang->getVariants() ) > 1 |
||
| 89 | && !is_null( $ret ) && $ret->getArticleID() == 0 |
||
| 90 | ) { |
||
| 91 | $wgContLang->findVariantLink( $title, $ret ); |
||
| 92 | } |
||
| 93 | } |
||
| 94 | |||
| 95 | // If title is not provided, always allow oldid and diff to set the title. |
||
| 96 | // If title is provided, allow oldid and diff to override the title, unless |
||
| 97 | // we are talking about a special page which might use these parameters for |
||
| 98 | // other purposes. |
||
| 99 | if ( $ret === null || !$ret->isSpecialPage() ) { |
||
| 100 | // We can have urls with just ?diff=,?oldid= or even just ?diff= |
||
| 101 | $oldid = $request->getInt( 'oldid' ); |
||
| 102 | $oldid = $oldid ? $oldid : $request->getInt( 'diff' ); |
||
| 103 | // Allow oldid to override a changed or missing title |
||
| 104 | if ( $oldid ) { |
||
| 105 | $rev = Revision::newFromId( $oldid ); |
||
| 106 | $ret = $rev ? $rev->getTitle() : $ret; |
||
| 107 | } |
||
| 108 | } |
||
| 109 | |||
| 110 | // Use the main page as default title if nothing else has been provided |
||
| 111 | if ( $ret === null |
||
| 112 | && strval( $title ) === '' |
||
| 113 | && !$request->getCheck( 'curid' ) |
||
| 114 | && $action !== 'delete' |
||
| 115 | ) { |
||
| 116 | $ret = Title::newMainPage(); |
||
| 117 | } |
||
| 118 | |||
| 119 | if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) { |
||
| 120 | // If we get here, we definitely don't have a valid title; throw an exception. |
||
| 121 | // Try to get detailed invalid title exception first, fall back to MalformedTitleException. |
||
| 122 | Title::newFromTextThrow( $title ); |
||
| 123 | throw new MalformedTitleException( 'badtitletext', $title ); |
||
| 124 | } |
||
| 125 | |||
| 126 | return $ret; |
||
| 127 | } |
||
| 128 | |||
| 129 | /** |
||
| 130 | * Get the Title object that we'll be acting on, as specified in the WebRequest |
||
| 131 | * @return Title |
||
| 132 | */ |
||
| 133 | public function getTitle() { |
||
| 134 | if ( !$this->context->hasTitle() ) { |
||
| 135 | try { |
||
| 136 | $this->context->setTitle( $this->parseTitle() ); |
||
| 137 | } catch ( MalformedTitleException $ex ) { |
||
| 138 | $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); |
||
| 139 | } |
||
| 140 | } |
||
| 141 | return $this->context->getTitle(); |
||
| 142 | } |
||
| 143 | |||
| 144 | /** |
||
| 145 | * Returns the name of the action that will be executed. |
||
| 146 | * |
||
| 147 | * @return string Action |
||
| 148 | */ |
||
| 149 | public function getAction() { |
||
| 150 | if ( $this->action === null ) { |
||
| 151 | $this->action = Action::getActionName( $this->context ); |
||
| 152 | } |
||
| 153 | |||
| 154 | return $this->action; |
||
| 155 | } |
||
| 156 | |||
| 157 | /** |
||
| 158 | * Performs the request. |
||
| 159 | * - bad titles |
||
| 160 | * - read restriction |
||
| 161 | * - local interwiki redirects |
||
| 162 | * - redirect loop |
||
| 163 | * - special pages |
||
| 164 | * - normal pages |
||
| 165 | * |
||
| 166 | * @throws MWException|PermissionsError|BadTitleError|HttpError |
||
| 167 | * @return void |
||
| 168 | */ |
||
| 169 | private function performRequest() { |
||
| 170 | global $wgTitle; |
||
| 171 | |||
| 172 | $request = $this->context->getRequest(); |
||
| 173 | $requestTitle = $title = $this->context->getTitle(); |
||
| 174 | $output = $this->context->getOutput(); |
||
| 175 | $user = $this->context->getUser(); |
||
| 176 | |||
| 177 | if ( $request->getVal( 'printable' ) === 'yes' ) { |
||
| 178 | $output->setPrintable(); |
||
| 179 | } |
||
| 180 | |||
| 181 | $unused = null; // To pass it by reference |
||
| 182 | Hooks::run( 'BeforeInitialize', [ &$title, &$unused, &$output, &$user, $request, $this ] ); |
||
| 183 | |||
| 184 | // Invalid titles. Bug 21776: The interwikis must redirect even if the page name is empty. |
||
| 185 | if ( is_null( $title ) || ( $title->getDBkey() == '' && !$title->isExternal() ) |
||
| 186 | || $title->isSpecial( 'Badtitle' ) |
||
| 187 | ) { |
||
| 188 | $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); |
||
| 189 | try { |
||
| 190 | $this->parseTitle(); |
||
| 191 | } catch ( MalformedTitleException $ex ) { |
||
| 192 | throw new BadTitleError( $ex ); |
||
| 193 | } |
||
| 194 | throw new BadTitleError(); |
||
| 195 | } |
||
| 196 | |||
| 197 | // Check user's permissions to read this page. |
||
| 198 | // We have to check here to catch special pages etc. |
||
| 199 | // We will check again in Article::view(). |
||
| 200 | $permErrors = $title->isSpecial( 'RunJobs' ) |
||
| 201 | ? [] // relies on HMAC key signature alone |
||
| 202 | : $title->getUserPermissionsErrors( 'read', $user ); |
||
| 203 | if ( count( $permErrors ) ) { |
||
| 204 | // Bug 32276: allowing the skin to generate output with $wgTitle or |
||
| 205 | // $this->context->title set to the input title would allow anonymous users to |
||
| 206 | // determine whether a page exists, potentially leaking private data. In fact, the |
||
| 207 | // curid and oldid request parameters would allow page titles to be enumerated even |
||
| 208 | // when they are not guessable. So we reset the title to Special:Badtitle before the |
||
| 209 | // permissions error is displayed. |
||
| 210 | |||
| 211 | // The skin mostly uses $this->context->getTitle() these days, but some extensions |
||
| 212 | // still use $wgTitle. |
||
| 213 | $badTitle = SpecialPage::getTitleFor( 'Badtitle' ); |
||
| 214 | $this->context->setTitle( $badTitle ); |
||
| 215 | $wgTitle = $badTitle; |
||
| 216 | |||
| 217 | throw new PermissionsError( 'read', $permErrors ); |
||
| 218 | } |
||
| 219 | |||
| 220 | // Interwiki redirects |
||
| 221 | if ( $title->isExternal() ) { |
||
| 222 | $rdfrom = $request->getVal( 'rdfrom' ); |
||
| 223 | if ( $rdfrom ) { |
||
| 224 | $url = $title->getFullURL( [ 'rdfrom' => $rdfrom ] ); |
||
| 225 | } else { |
||
| 226 | $query = $request->getValues(); |
||
| 227 | unset( $query['title'] ); |
||
| 228 | $url = $title->getFullURL( $query ); |
||
| 229 | } |
||
| 230 | // Check for a redirect loop |
||
| 231 | if ( !preg_match( '/^' . preg_quote( $this->config->get( 'Server' ), '/' ) . '/', $url ) |
||
| 232 | && $title->isLocal() |
||
| 233 | ) { |
||
| 234 | // 301 so google et al report the target as the actual url. |
||
| 235 | $output->redirect( $url, 301 ); |
||
| 236 | } else { |
||
| 237 | $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); |
||
| 238 | try { |
||
| 239 | $this->parseTitle(); |
||
| 240 | } catch ( MalformedTitleException $ex ) { |
||
| 241 | throw new BadTitleError( $ex ); |
||
| 242 | } |
||
| 243 | throw new BadTitleError(); |
||
| 244 | } |
||
| 245 | // Handle any other redirects. |
||
| 246 | // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant |
||
| 247 | } elseif ( !$this->tryNormaliseRedirect( $title ) ) { |
||
| 248 | // Prevent information leak via Special:MyPage et al (T109724) |
||
| 249 | if ( $title->isSpecialPage() ) { |
||
| 250 | $specialPage = SpecialPageFactory::getPage( $title->getDBkey() ); |
||
| 251 | if ( $specialPage instanceof RedirectSpecialPage ) { |
||
| 252 | $specialPage->setContext( $this->context ); |
||
| 253 | if ( $this->config->get( 'HideIdentifiableRedirects' ) |
||
| 254 | && $specialPage->personallyIdentifiableTarget() |
||
| 255 | ) { |
||
| 256 | list( , $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); |
||
| 257 | $target = $specialPage->getRedirect( $subpage ); |
||
| 258 | // target can also be true. We let that case fall through to normal processing. |
||
| 259 | if ( $target instanceof Title ) { |
||
| 260 | $query = $specialPage->getRedirectQuery() ?: []; |
||
| 261 | $request = new DerivativeRequest( $this->context->getRequest(), $query ); |
||
| 262 | $request->setRequestURL( $this->context->getRequest()->getRequestURL() ); |
||
| 263 | $this->context->setRequest( $request ); |
||
| 264 | // Do not varnish cache these. May vary even for anons |
||
| 265 | $this->context->getOutput()->lowerCdnMaxage( 0 ); |
||
| 266 | $this->context->setTitle( $target ); |
||
| 267 | $wgTitle = $target; |
||
| 268 | // Reset action type cache. (Special pages have only view) |
||
| 269 | $this->action = null; |
||
| 270 | $title = $target; |
||
| 271 | $output->addJsConfigVars( [ |
||
| 272 | 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ), |
||
| 273 | ] ); |
||
| 274 | $output->addModules( 'mediawiki.action.view.redirect' ); |
||
| 275 | } |
||
| 276 | } |
||
| 277 | } |
||
| 278 | } |
||
| 279 | |||
| 280 | // Special pages ($title may have changed since if statement above) |
||
| 281 | if ( NS_SPECIAL == $title->getNamespace() ) { |
||
| 282 | // Actions that need to be made when we have a special pages |
||
| 283 | SpecialPageFactory::executePath( $title, $this->context ); |
||
| 284 | } else { |
||
| 285 | // ...otherwise treat it as an article view. The article |
||
| 286 | // may still be a wikipage redirect to another article or URL. |
||
| 287 | $article = $this->initializeArticle(); |
||
| 288 | if ( is_object( $article ) ) { |
||
| 289 | $this->performAction( $article, $requestTitle ); |
||
| 290 | } elseif ( is_string( $article ) ) { |
||
| 291 | $output->redirect( $article ); |
||
| 292 | } else { |
||
| 293 | throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" |
||
| 294 | . " returned neither an object nor a URL" ); |
||
| 295 | } |
||
| 296 | } |
||
| 297 | } |
||
| 298 | } |
||
| 299 | |||
| 300 | /** |
||
| 301 | * Handle redirects for uncanonical title requests. |
||
| 302 | * |
||
| 303 | * Handles: |
||
| 304 | * - Redirect loops. |
||
| 305 | * - No title in URL. |
||
| 306 | * - $wgUsePathInfo URLs. |
||
| 307 | * - URLs with a variant. |
||
| 308 | * - Other non-standard URLs (as long as they have no extra query parameters). |
||
| 309 | * |
||
| 310 | * Behaviour: |
||
| 311 | * - Normalise title values: |
||
| 312 | * /wiki/Foo%20Bar -> /wiki/Foo_Bar |
||
| 313 | * - Normalise empty title: |
||
| 314 | * /wiki/ -> /wiki/Main |
||
| 315 | * /w/index.php?title= -> /wiki/Main |
||
| 316 | * - Don't redirect anything with query parameters other than 'title' or 'action=view'. |
||
| 317 | * |
||
| 318 | * @param Title $title |
||
| 319 | * @return bool True if a redirect was set. |
||
| 320 | * @throws HttpError |
||
| 321 | */ |
||
| 322 | private function tryNormaliseRedirect( Title $title ) { |
||
| 323 | $request = $this->context->getRequest(); |
||
| 324 | $output = $this->context->getOutput(); |
||
| 325 | |||
| 326 | if ( $request->getVal( 'action', 'view' ) != 'view' |
||
| 327 | || $request->wasPosted() |
||
| 328 | || ( $request->getVal( 'title' ) !== null |
||
| 329 | && $title->getPrefixedDBkey() == $request->getVal( 'title' ) ) |
||
| 330 | || count( $request->getValueNames( [ 'action', 'title' ] ) ) |
||
| 331 | || !Hooks::run( 'TestCanonicalRedirect', [ $request, $title, $output ] ) |
||
| 332 | ) { |
||
| 333 | return false; |
||
| 334 | } |
||
| 335 | |||
| 336 | if ( $title->isSpecialPage() ) { |
||
| 337 | list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); |
||
| 338 | if ( $name ) { |
||
| 339 | $title = SpecialPage::getTitleFor( $name, $subpage ); |
||
| 340 | } |
||
| 341 | } |
||
| 342 | // Redirect to canonical url, make it a 301 to allow caching |
||
| 343 | $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); |
||
| 344 | if ( $targetUrl == $request->getFullRequestURL() ) { |
||
| 345 | $message = "Redirect loop detected!\n\n" . |
||
| 346 | "This means the wiki got confused about what page was " . |
||
| 347 | "requested; this sometimes happens when moving a wiki " . |
||
| 348 | "to a new server or changing the server configuration.\n\n"; |
||
| 349 | |||
| 350 | if ( $this->config->get( 'UsePathInfo' ) ) { |
||
| 351 | $message .= "The wiki is trying to interpret the page " . |
||
| 352 | "title from the URL path portion (PATH_INFO), which " . |
||
| 353 | "sometimes fails depending on the web server. Try " . |
||
| 354 | "setting \"\$wgUsePathInfo = false;\" in your " . |
||
| 355 | "LocalSettings.php, or check that \$wgArticlePath " . |
||
| 356 | "is correct."; |
||
| 357 | } else { |
||
| 358 | $message .= "Your web server was detected as possibly not " . |
||
| 359 | "supporting URL path components (PATH_INFO) correctly; " . |
||
| 360 | "check your LocalSettings.php for a customized " . |
||
| 361 | "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . |
||
| 362 | "to true."; |
||
| 363 | } |
||
| 364 | throw new HttpError( 500, $message ); |
||
| 365 | } |
||
| 366 | $output->setSquidMaxage( 1200 ); |
||
| 367 | $output->redirect( $targetUrl, '301' ); |
||
| 368 | return true; |
||
| 369 | } |
||
| 370 | |||
| 371 | /** |
||
| 372 | * Initialize the main Article object for "standard" actions (view, etc) |
||
| 373 | * Create an Article object for the page, following redirects if needed. |
||
| 374 | * |
||
| 375 | * @return Article|string An Article, or a string to redirect to another URL |
||
| 376 | */ |
||
| 377 | private function initializeArticle() { |
||
| 378 | $title = $this->context->getTitle(); |
||
| 379 | if ( $this->context->canUseWikiPage() ) { |
||
| 380 | // Try to use request context wiki page, as there |
||
| 381 | // is already data from db saved in per process |
||
| 382 | // cache there from this->getAction() call. |
||
| 383 | $page = $this->context->getWikiPage(); |
||
| 384 | } else { |
||
| 385 | // This case should not happen, but just in case. |
||
| 386 | // @TODO: remove this or use an exception |
||
| 387 | $page = WikiPage::factory( $title ); |
||
| 388 | $this->context->setWikiPage( $page ); |
||
| 389 | wfWarn( "RequestContext::canUseWikiPage() returned false" ); |
||
| 390 | } |
||
| 391 | |||
| 392 | // Make GUI wrapper for the WikiPage |
||
| 393 | $article = Article::newFromWikiPage( $page, $this->context ); |
||
| 394 | |||
| 395 | // Skip some unnecessary code if the content model doesn't support redirects |
||
| 396 | if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) { |
||
| 397 | return $article; |
||
| 398 | } |
||
| 399 | |||
| 400 | $request = $this->context->getRequest(); |
||
| 401 | |||
| 402 | // Namespace might change when using redirects |
||
| 403 | // Check for redirects ... |
||
| 404 | $action = $request->getVal( 'action', 'view' ); |
||
| 405 | $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null; |
||
| 406 | if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content |
||
| 407 | && !$request->getVal( 'oldid' ) // ... and are not old revisions |
||
| 408 | && !$request->getVal( 'diff' ) // ... and not when showing diff |
||
| 409 | && $request->getVal( 'redirect' ) != 'no' // ... unless explicitly told not to |
||
| 410 | // ... and the article is not a non-redirect image page with associated file |
||
| 411 | && !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) |
||
| 412 | ) { |
||
| 413 | // Give extensions a change to ignore/handle redirects as needed |
||
| 414 | $ignoreRedirect = $target = false; |
||
| 415 | |||
| 416 | Hooks::run( 'InitializeArticleMaybeRedirect', |
||
| 417 | [ &$title, &$request, &$ignoreRedirect, &$target, &$article ] ); |
||
| 418 | $page = $article->getPage(); // reflect any hook changes |
||
| 419 | |||
| 420 | // Follow redirects only for... redirects. |
||
| 421 | // If $target is set, then a hook wanted to redirect. |
||
| 422 | if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) { |
||
| 423 | // Is the target already set by an extension? |
||
| 424 | $target = $target ? $target : $page->followRedirect(); |
||
| 425 | if ( is_string( $target ) ) { |
||
| 426 | if ( !$this->config->get( 'DisableHardRedirects' ) ) { |
||
| 427 | // we'll need to redirect |
||
| 428 | return $target; |
||
| 429 | } |
||
| 430 | } |
||
| 431 | if ( is_object( $target ) ) { |
||
| 432 | // Rewrite environment to redirected article |
||
| 433 | $rpage = WikiPage::factory( $target ); |
||
| 434 | $rpage->loadPageData(); |
||
| 435 | if ( $rpage->exists() || ( is_object( $file ) && !$file->isLocal() ) ) { |
||
| 436 | $rarticle = Article::newFromWikiPage( $rpage, $this->context ); |
||
| 437 | $rarticle->setRedirectedFrom( $title ); |
||
| 438 | |||
| 439 | $article = $rarticle; |
||
| 440 | $this->context->setTitle( $target ); |
||
| 441 | $this->context->setWikiPage( $article->getPage() ); |
||
| 442 | } |
||
| 443 | } |
||
| 444 | } else { |
||
| 445 | // Article may have been changed by hook |
||
| 446 | $this->context->setTitle( $article->getTitle() ); |
||
| 447 | $this->context->setWikiPage( $article->getPage() ); |
||
| 448 | } |
||
| 449 | } |
||
| 450 | |||
| 451 | return $article; |
||
| 452 | } |
||
| 453 | |||
| 454 | /** |
||
| 455 | * Perform one of the "standard" actions |
||
| 456 | * |
||
| 457 | * @param Page $page |
||
| 458 | * @param Title $requestTitle The original title, before any redirects were applied |
||
| 459 | */ |
||
| 460 | private function performAction( Page $page, Title $requestTitle ) { |
||
| 461 | $request = $this->context->getRequest(); |
||
| 462 | $output = $this->context->getOutput(); |
||
| 463 | $title = $this->context->getTitle(); |
||
| 464 | $user = $this->context->getUser(); |
||
| 465 | |||
| 466 | if ( !Hooks::run( 'MediaWikiPerformAction', |
||
| 467 | [ $output, $page, $title, $user, $request, $this ] ) |
||
| 468 | ) { |
||
| 469 | return; |
||
| 470 | } |
||
| 471 | |||
| 472 | $act = $this->getAction(); |
||
| 473 | $action = Action::factory( $act, $page, $this->context ); |
||
| 474 | |||
| 475 | if ( $action instanceof Action ) { |
||
| 476 | // Narrow DB query expectations for this HTTP request |
||
| 477 | $trxLimits = $this->config->get( 'TrxProfilerLimits' ); |
||
| 478 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
||
| 479 | if ( $request->wasPosted() && !$action->doesWrites() ) { |
||
| 480 | $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ ); |
||
| 481 | $request->markAsSafeRequest(); |
||
| 482 | } |
||
| 483 | |||
| 484 | # Let CDN cache things if we can purge them. |
||
| 485 | if ( $this->config->get( 'UseSquid' ) && |
||
| 486 | in_array( |
||
| 487 | // Use PROTO_INTERNAL because that's what getCdnUrls() uses |
||
| 488 | wfExpandUrl( $request->getRequestURL(), PROTO_INTERNAL ), |
||
| 489 | $requestTitle->getCdnUrls() |
||
| 490 | ) |
||
| 491 | ) { |
||
| 492 | $output->setCdnMaxage( $this->config->get( 'SquidMaxage' ) ); |
||
| 493 | } |
||
| 494 | |||
| 495 | $action->show(); |
||
| 496 | return; |
||
| 497 | } |
||
| 498 | |||
| 499 | if ( Hooks::run( 'UnknownAction', [ $request->getVal( 'action', 'view' ), $page ] ) ) { |
||
| 500 | $output->setStatusCode( 404 ); |
||
| 501 | $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); |
||
| 502 | } |
||
| 503 | } |
||
| 504 | |||
| 505 | /** |
||
| 506 | * Run the current MediaWiki instance; index.php just calls this |
||
| 507 | */ |
||
| 508 | public function run() { |
||
| 509 | try { |
||
| 510 | $this->setDBProfilingAgent(); |
||
| 511 | try { |
||
| 512 | $this->main(); |
||
| 513 | } catch ( ErrorPageError $e ) { |
||
| 514 | // Bug 62091: while exceptions are convenient to bubble up GUI errors, |
||
| 515 | // they are not internal application faults. As with normal requests, this |
||
| 516 | // should commit, print the output, do deferred updates, jobs, and profiling. |
||
| 517 | $this->doPreOutputCommit(); |
||
| 518 | $e->report(); // display the GUI error |
||
| 519 | } |
||
| 520 | } catch ( Exception $e ) { |
||
| 521 | $context = $this->context; |
||
| 522 | $action = $context->getRequest()->getVal( 'action', 'view' ); |
||
| 523 | if ( |
||
| 524 | $e instanceof DBConnectionError && |
||
| 525 | $context->hasTitle() && |
||
| 526 | $context->getTitle()->canExist() && |
||
| 527 | in_array( $action, [ 'view', 'history' ], true ) && |
||
| 528 | HTMLFileCache::useFileCache( $this->context, HTMLFileCache::MODE_OUTAGE ) |
||
| 529 | ) { |
||
| 530 | // Try to use any (even stale) file during outages... |
||
| 531 | $cache = new HTMLFileCache( $context->getTitle(), 'view' ); |
||
| 532 | if ( $cache->isCached() ) { |
||
| 533 | $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE ); |
||
| 534 | print MWExceptionRenderer::getHTML( $e ); |
||
| 535 | exit; |
||
| 536 | } |
||
| 537 | |||
| 538 | } |
||
| 539 | |||
| 540 | MWExceptionHandler::handleException( $e ); |
||
| 541 | } |
||
| 542 | |||
| 543 | $this->doPostOutputShutdown( 'normal' ); |
||
| 544 | } |
||
| 545 | |||
| 546 | private function setDBProfilingAgent() { |
||
| 547 | $services = MediaWikiServices::getInstance(); |
||
| 548 | // Add a comment for easy SHOW PROCESSLIST interpretation |
||
| 549 | $name = $this->context->getUser()->getName(); |
||
| 550 | $services->getDBLoadBalancerFactory()->setAgentName( |
||
| 551 | mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) . '...' : $name |
||
| 552 | ); |
||
| 553 | } |
||
| 554 | |||
| 555 | /** |
||
| 556 | * @see MediaWiki::preOutputCommit() |
||
| 557 | * @param callable $postCommitWork [default: null] |
||
| 558 | * @since 1.26 |
||
| 559 | */ |
||
| 560 | public function doPreOutputCommit( callable $postCommitWork = null ) { |
||
| 561 | self::preOutputCommit( $this->context, $postCommitWork ); |
||
| 562 | } |
||
| 563 | |||
| 564 | /** |
||
| 565 | * This function commits all DB changes as needed before |
||
| 566 | * the user can receive a response (in case commit fails) |
||
| 567 | * |
||
| 568 | * @param IContextSource $context |
||
| 569 | * @param callable $postCommitWork [default: null] |
||
| 570 | * @since 1.27 |
||
| 571 | */ |
||
| 572 | public static function preOutputCommit( |
||
| 573 | IContextSource $context, callable $postCommitWork = null |
||
| 574 | ) { |
||
| 575 | // Either all DBs should commit or none |
||
| 576 | ignore_user_abort( true ); |
||
| 577 | |||
| 578 | $config = $context->getConfig(); |
||
| 579 | $request = $context->getRequest(); |
||
| 580 | $output = $context->getOutput(); |
||
| 581 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
||
| 582 | |||
| 583 | // Commit all changes |
||
| 584 | $lbFactory->commitMasterChanges( |
||
| 585 | __METHOD__, |
||
| 586 | // Abort if any transaction was too big |
||
| 587 | [ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ] |
||
| 588 | ); |
||
| 589 | wfDebug( __METHOD__ . ': primary transaction round committed' ); |
||
| 590 | |||
| 591 | // Run updates that need to block the user or affect output (this is the last chance) |
||
| 592 | DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND ); |
||
| 593 | wfDebug( __METHOD__ . ': pre-send deferred updates completed' ); |
||
| 594 | |||
| 595 | // Decide when clients block on ChronologyProtector DB position writes |
||
| 596 | $urlDomainDistance = ( |
||
| 597 | $request->wasPosted() && |
||
| 598 | $output->getRedirect() && |
||
| 599 | $lbFactory->hasOrMadeRecentMasterChanges( INF ) |
||
| 600 | ) ? self::getUrlDomainDistance( $output->getRedirect(), $context ) : false; |
||
| 601 | |||
| 602 | if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) { |
||
| 603 | // OutputPage::output() will be fast; $postCommitWork will not be useful for |
||
| 604 | // masking the latency of syncing DB positions accross all datacenters synchronously. |
||
| 605 | // Instead, make use of the RTT time of the client follow redirects. |
||
| 606 | $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC; |
||
| 607 | $cpPosTime = microtime( true ); |
||
| 608 | // Client's next request should see 1+ positions with this DBMasterPos::asOf() time |
||
| 609 | if ( $urlDomainDistance === 'local' ) { |
||
| 610 | // Client will stay on this domain, so set an unobtrusive cookie |
||
| 611 | $expires = time() + ChronologyProtector::POSITION_TTL; |
||
| 612 | $options = [ 'prefix' => '' ]; |
||
| 613 | $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options ); |
||
| 614 | } else { |
||
| 615 | // Cookies may not work across wiki domains, so use a URL parameter |
||
| 616 | $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery( |
||
| 617 | $output->getRedirect(), |
||
| 618 | $cpPosTime |
||
| 619 | ); |
||
| 620 | $output->redirect( $safeUrl ); |
||
| 621 | } |
||
| 622 | } else { |
||
| 623 | // OutputPage::output() is fairly slow; run it in $postCommitWork to mask |
||
| 624 | // the latency of syncing DB positions accross all datacenters synchronously |
||
| 625 | $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC; |
||
| 626 | if ( $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) { |
||
| 627 | $cpPosTime = microtime( true ); |
||
| 628 | // Set a cookie in case the DB position store cannot sync accross datacenters. |
||
| 629 | // This will at least cover the common case of the user staying on the domain. |
||
| 630 | $expires = time() + ChronologyProtector::POSITION_TTL; |
||
| 631 | $options = [ 'prefix' => '' ]; |
||
| 632 | $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options ); |
||
| 633 | } |
||
| 634 | } |
||
| 635 | // Record ChronologyProtector positions for DBs affected in this request at this point |
||
| 636 | $lbFactory->shutdown( $flags, $postCommitWork ); |
||
| 637 | wfDebug( __METHOD__ . ': LBFactory shutdown completed' ); |
||
| 638 | |||
| 639 | // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this |
||
| 640 | // POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so |
||
| 641 | // ChronologyProtector works for cacheable URLs. |
||
| 642 | if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) { |
||
| 643 | $expires = time() + $config->get( 'DataCenterUpdateStickTTL' ); |
||
| 644 | $options = [ 'prefix' => '' ]; |
||
| 645 | $request->response()->setCookie( 'UseDC', 'master', $expires, $options ); |
||
| 646 | $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options ); |
||
| 647 | } |
||
| 648 | |||
| 649 | // Avoid letting a few seconds of replica DB lag cause a month of stale data. This logic is |
||
| 650 | // also intimately related to the value of $wgCdnReboundPurgeDelay. |
||
| 651 | if ( $lbFactory->laggedReplicaUsed() ) { |
||
| 652 | $maxAge = $config->get( 'CdnMaxageLagged' ); |
||
| 653 | $output->lowerCdnMaxage( $maxAge ); |
||
| 654 | $request->response()->header( "X-Database-Lagged: true" ); |
||
| 655 | wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" ); |
||
| 656 | } |
||
| 657 | |||
| 658 | // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069) |
||
| 659 | if ( MessageCache::singleton()->isDisabled() ) { |
||
| 660 | $maxAge = $config->get( 'CdnMaxageSubstitute' ); |
||
| 661 | $output->lowerCdnMaxage( $maxAge ); |
||
| 662 | $request->response()->header( "X-Response-Substitute: true" ); |
||
| 663 | } |
||
| 664 | } |
||
| 665 | |||
| 666 | /** |
||
| 667 | * @param string $url |
||
| 668 | * @param IContextSource $context |
||
| 669 | * @return string Either "local", "remote" if in the farm, "external" otherwise |
||
| 670 | */ |
||
| 671 | private static function getUrlDomainDistance( $url, IContextSource $context ) { |
||
| 672 | static $relevantKeys = [ 'host' => true, 'port' => true ]; |
||
| 673 | |||
| 674 | $infoCandidate = wfParseUrl( $url ); |
||
| 675 | if ( $infoCandidate === false ) { |
||
| 676 | return 'external'; |
||
| 677 | } |
||
| 678 | |||
| 679 | $infoCandidate = array_intersect_key( $infoCandidate, $relevantKeys ); |
||
| 680 | $clusterHosts = array_merge( |
||
| 681 | // Local wiki host (the most common case) |
||
| 682 | [ $context->getConfig()->get( 'CanonicalServer' ) ], |
||
| 683 | // Any local/remote wiki virtual hosts for this wiki farm |
||
| 684 | $context->getConfig()->get( 'LocalVirtualHosts' ) |
||
| 685 | ); |
||
| 686 | |||
| 687 | foreach ( $clusterHosts as $i => $clusterHost ) { |
||
| 688 | $parseUrl = wfParseUrl( $clusterHost ); |
||
| 689 | if ( !$parseUrl ) { |
||
| 690 | continue; |
||
| 691 | } |
||
| 692 | $infoHost = array_intersect_key( $parseUrl, $relevantKeys ); |
||
| 693 | if ( $infoCandidate === $infoHost ) { |
||
| 694 | return ( $i === 0 ) ? 'local' : 'remote'; |
||
| 695 | } |
||
| 696 | } |
||
| 697 | |||
| 698 | return 'external'; |
||
| 699 | } |
||
| 700 | |||
| 701 | /** |
||
| 702 | * This function does work that can be done *after* the |
||
| 703 | * user gets the HTTP response so they don't block on it |
||
| 704 | * |
||
| 705 | * This manages deferred updates, job insertion, |
||
| 706 | * final commit, and the logging of profiling data |
||
| 707 | * |
||
| 708 | * @param string $mode Use 'fast' to always skip job running |
||
| 709 | * @since 1.26 |
||
| 710 | */ |
||
| 711 | public function doPostOutputShutdown( $mode = 'normal' ) { |
||
| 712 | $timing = $this->context->getTiming(); |
||
| 713 | $timing->mark( 'requestShutdown' ); |
||
| 714 | |||
| 715 | // Show visible profiling data if enabled (which cannot be post-send) |
||
| 716 | Profiler::instance()->logDataPageOutputOnly(); |
||
| 717 | |||
| 718 | $callback = function () use ( $mode ) { |
||
| 719 | try { |
||
| 720 | $this->restInPeace( $mode ); |
||
| 721 | } catch ( Exception $e ) { |
||
| 722 | MWExceptionHandler::handleException( $e ); |
||
| 723 | } |
||
| 724 | }; |
||
| 725 | |||
| 726 | // Defer everything else... |
||
| 727 | if ( function_exists( 'register_postsend_function' ) ) { |
||
| 728 | // https://github.com/facebook/hhvm/issues/1230 |
||
| 729 | register_postsend_function( $callback ); |
||
| 730 | } else { |
||
| 731 | if ( function_exists( 'fastcgi_finish_request' ) ) { |
||
| 732 | fastcgi_finish_request(); |
||
| 733 | } else { |
||
| 734 | // Either all DB and deferred updates should happen or none. |
||
| 735 | // The latter should not be cancelled due to client disconnect. |
||
| 736 | ignore_user_abort( true ); |
||
| 737 | } |
||
| 738 | |||
| 739 | $callback(); |
||
| 740 | } |
||
| 741 | } |
||
| 742 | |||
| 743 | private function main() { |
||
| 744 | global $wgTitle; |
||
| 745 | |||
| 746 | $output = $this->context->getOutput(); |
||
| 747 | $request = $this->context->getRequest(); |
||
| 748 | |||
| 749 | // Send Ajax requests to the Ajax dispatcher. |
||
| 750 | if ( $this->config->get( 'UseAjax' ) && $request->getVal( 'action' ) === 'ajax' ) { |
||
| 751 | // Set a dummy title, because $wgTitle == null might break things |
||
| 752 | $title = Title::makeTitle( NS_SPECIAL, 'Badtitle/performing an AJAX call in ' |
||
| 753 | . __METHOD__ |
||
| 754 | ); |
||
| 755 | $this->context->setTitle( $title ); |
||
| 756 | $wgTitle = $title; |
||
| 757 | |||
| 758 | $dispatcher = new AjaxDispatcher( $this->config ); |
||
| 759 | $dispatcher->performAction( $this->context->getUser() ); |
||
| 760 | |||
| 761 | return; |
||
| 762 | } |
||
| 763 | |||
| 764 | // Get title from request parameters, |
||
| 765 | // is set on the fly by parseTitle the first time. |
||
| 766 | $title = $this->getTitle(); |
||
| 767 | $action = $this->getAction(); |
||
| 768 | $wgTitle = $title; |
||
| 769 | |||
| 770 | // Set DB query expectations for this HTTP request |
||
| 771 | $trxLimits = $this->config->get( 'TrxProfilerLimits' ); |
||
| 772 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
||
| 773 | $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) ); |
||
| 774 | if ( $request->hasSafeMethod() ) { |
||
| 775 | $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ ); |
||
| 776 | } else { |
||
| 777 | $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ ); |
||
| 778 | } |
||
| 779 | |||
| 780 | // If the user has forceHTTPS set to true, or if the user |
||
| 781 | // is in a group requiring HTTPS, or if they have the HTTPS |
||
| 782 | // preference set, redirect them to HTTPS. |
||
| 783 | // Note: Do this after $wgTitle is setup, otherwise the hooks run from |
||
| 784 | // isLoggedIn() will do all sorts of weird stuff. |
||
| 785 | if ( |
||
| 786 | $request->getProtocol() == 'http' && |
||
| 787 | // switch to HTTPS only when supported by the server |
||
| 788 | preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) && |
||
| 789 | ( |
||
| 790 | $request->getSession()->shouldForceHTTPS() || |
||
| 791 | // Check the cookie manually, for paranoia |
||
| 792 | $request->getCookie( 'forceHTTPS', '' ) || |
||
| 793 | // check for prefixed version that was used for a time in older MW versions |
||
| 794 | $request->getCookie( 'forceHTTPS' ) || |
||
| 795 | // Avoid checking the user and groups unless it's enabled. |
||
| 796 | ( |
||
| 797 | $this->context->getUser()->isLoggedIn() |
||
| 798 | && $this->context->getUser()->requiresHTTPS() |
||
| 799 | ) |
||
| 800 | ) |
||
| 801 | ) { |
||
| 802 | $oldUrl = $request->getFullRequestURL(); |
||
| 803 | $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl ); |
||
| 804 | |||
| 805 | // ATTENTION: This hook is likely to be removed soon due to overall design of the system. |
||
| 806 | if ( Hooks::run( 'BeforeHttpsRedirect', [ $this->context, &$redirUrl ] ) ) { |
||
| 807 | |||
| 808 | if ( $request->wasPosted() ) { |
||
| 809 | // This is weird and we'd hope it almost never happens. This |
||
| 810 | // means that a POST came in via HTTP and policy requires us |
||
| 811 | // redirecting to HTTPS. It's likely such a request is going |
||
| 812 | // to fail due to post data being lost, but let's try anyway |
||
| 813 | // and just log the instance. |
||
| 814 | |||
| 815 | // @todo FIXME: See if we could issue a 307 or 308 here, need |
||
| 816 | // to see how clients (automated & browser) behave when we do |
||
| 817 | wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" ); |
||
| 818 | } |
||
| 819 | // Setup dummy Title, otherwise OutputPage::redirect will fail |
||
| 820 | $title = Title::newFromText( 'REDIR', NS_MAIN ); |
||
| 821 | $this->context->setTitle( $title ); |
||
|
0 ignored issues
–
show
|
|||
| 822 | // Since we only do this redir to change proto, always send a vary header |
||
| 823 | $output->addVaryHeader( 'X-Forwarded-Proto' ); |
||
| 824 | $output->redirect( $redirUrl ); |
||
| 825 | $output->output(); |
||
| 826 | |||
| 827 | return; |
||
| 828 | } |
||
| 829 | } |
||
| 830 | |||
| 831 | if ( $title->canExist() && HTMLFileCache::useFileCache( $this->context ) ) { |
||
| 832 | // Try low-level file cache hit |
||
| 833 | $cache = new HTMLFileCache( $title, $action ); |
||
| 834 | if ( $cache->isCacheGood( /* Assume up to date */ ) ) { |
||
| 835 | // Check incoming headers to see if client has this cached |
||
| 836 | $timestamp = $cache->cacheTimestamp(); |
||
| 837 | if ( !$output->checkLastModified( $timestamp ) ) { |
||
| 838 | $cache->loadFromFileCache( $this->context ); |
||
| 839 | } |
||
| 840 | // Do any stats increment/watchlist stuff, assuming user is viewing the |
||
| 841 | // latest revision (which should always be the case for file cache) |
||
| 842 | $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() ); |
||
| 843 | // Tell OutputPage that output is taken care of |
||
| 844 | $output->disable(); |
||
| 845 | |||
| 846 | return; |
||
| 847 | } |
||
| 848 | } |
||
| 849 | |||
| 850 | // Actually do the work of the request and build up any output |
||
| 851 | $this->performRequest(); |
||
| 852 | |||
| 853 | // GUI-ify and stash the page output in MediaWiki::doPreOutputCommit() while |
||
| 854 | // ChronologyProtector synchronizes DB positions or slaves accross all datacenters. |
||
| 855 | $buffer = null; |
||
| 856 | $outputWork = function () use ( $output, &$buffer ) { |
||
| 857 | if ( $buffer === null ) { |
||
| 858 | $buffer = $output->output( true ); |
||
| 859 | } |
||
| 860 | |||
| 861 | return $buffer; |
||
| 862 | }; |
||
| 863 | |||
| 864 | // Now commit any transactions, so that unreported errors after |
||
| 865 | // output() don't roll back the whole DB transaction and so that |
||
| 866 | // we avoid having both success and error text in the response |
||
| 867 | $this->doPreOutputCommit( $outputWork ); |
||
| 868 | |||
| 869 | // Now send the actual output |
||
| 870 | print $outputWork(); |
||
| 871 | } |
||
| 872 | |||
| 873 | /** |
||
| 874 | * Ends this task peacefully |
||
| 875 | * @param string $mode Use 'fast' to always skip job running |
||
| 876 | */ |
||
| 877 | public function restInPeace( $mode = 'fast' ) { |
||
| 878 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
||
| 879 | // Assure deferred updates are not in the main transaction |
||
| 880 | $lbFactory->commitMasterChanges( __METHOD__ ); |
||
| 881 | |||
| 882 | // Loosen DB query expectations since the HTTP client is unblocked |
||
| 883 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
||
| 884 | $trxProfiler->resetExpectations(); |
||
| 885 | $trxProfiler->setExpectations( |
||
| 886 | $this->config->get( 'TrxProfilerLimits' )['PostSend'], |
||
| 887 | __METHOD__ |
||
| 888 | ); |
||
| 889 | |||
| 890 | // Do any deferred jobs |
||
| 891 | DeferredUpdates::doUpdates( 'enqueue' ); |
||
| 892 | DeferredUpdates::setImmediateMode( true ); |
||
| 893 | |||
| 894 | // Make sure any lazy jobs are pushed |
||
| 895 | JobQueueGroup::pushLazyJobs(); |
||
| 896 | |||
| 897 | // Now that everything specific to this request is done, |
||
| 898 | // try to occasionally run jobs (if enabled) from the queues |
||
| 899 | if ( $mode === 'normal' ) { |
||
| 900 | $this->triggerJobs(); |
||
| 901 | } |
||
| 902 | |||
| 903 | // Log profiling data, e.g. in the database or UDP |
||
| 904 | wfLogProfilingData(); |
||
| 905 | |||
| 906 | // Commit and close up! |
||
| 907 | $lbFactory->commitMasterChanges( __METHOD__ ); |
||
| 908 | $lbFactory->shutdown( LBFactory::SHUTDOWN_NO_CHRONPROT ); |
||
| 909 | |||
| 910 | wfDebug( "Request ended normally\n" ); |
||
| 911 | } |
||
| 912 | |||
| 913 | /** |
||
| 914 | * Potentially open a socket and sent an HTTP request back to the server |
||
| 915 | * to run a specified number of jobs. This registers a callback to cleanup |
||
| 916 | * the socket once it's done. |
||
| 917 | */ |
||
| 918 | public function triggerJobs() { |
||
| 919 | $jobRunRate = $this->config->get( 'JobRunRate' ); |
||
| 920 | if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) { |
||
| 921 | return; // recursion guard |
||
| 922 | } elseif ( $jobRunRate <= 0 || wfReadOnly() ) { |
||
| 923 | return; |
||
| 924 | } |
||
| 925 | |||
| 926 | if ( $jobRunRate < 1 ) { |
||
| 927 | $max = mt_getrandmax(); |
||
| 928 | if ( mt_rand( 0, $max ) > $max * $jobRunRate ) { |
||
| 929 | return; // the higher the job run rate, the less likely we return here |
||
| 930 | } |
||
| 931 | $n = 1; |
||
| 932 | } else { |
||
| 933 | $n = intval( $jobRunRate ); |
||
| 934 | } |
||
| 935 | |||
| 936 | $runJobsLogger = LoggerFactory::getInstance( 'runJobs' ); |
||
| 937 | |||
| 938 | // Fall back to running the job(s) while the user waits if needed |
||
| 939 | if ( !$this->config->get( 'RunJobsAsync' ) ) { |
||
| 940 | $runner = new JobRunner( $runJobsLogger ); |
||
| 941 | $runner->run( [ 'maxJobs' => $n ] ); |
||
| 942 | return; |
||
| 943 | } |
||
| 944 | |||
| 945 | // Do not send request if there are probably no jobs |
||
| 946 | try { |
||
| 947 | $group = JobQueueGroup::singleton(); |
||
| 948 | if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) { |
||
| 949 | return; |
||
| 950 | } |
||
| 951 | } catch ( JobQueueError $e ) { |
||
| 952 | MWExceptionHandler::logException( $e ); |
||
| 953 | return; // do not make the site unavailable |
||
| 954 | } |
||
| 955 | |||
| 956 | $query = [ 'title' => 'Special:RunJobs', |
||
| 957 | 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ]; |
||
| 958 | $query['signature'] = SpecialRunJobs::getQuerySignature( |
||
| 959 | $query, $this->config->get( 'SecretKey' ) ); |
||
| 960 | |||
| 961 | $errno = $errstr = null; |
||
| 962 | $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) ); |
||
| 963 | $host = $info ? $info['host'] : null; |
||
| 964 | $port = 80; |
||
| 965 | if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) { |
||
| 966 | $host = "tls://" . $host; |
||
| 967 | $port = 443; |
||
| 968 | } |
||
| 969 | if ( isset( $info['port'] ) ) { |
||
| 970 | $port = $info['port']; |
||
| 971 | } |
||
| 972 | |||
| 973 | MediaWiki\suppressWarnings(); |
||
| 974 | $sock = $host ? fsockopen( |
||
| 975 | $host, |
||
| 976 | $port, |
||
| 977 | $errno, |
||
| 978 | $errstr, |
||
| 979 | // If it takes more than 100ms to connect to ourselves there is a problem... |
||
| 980 | 0.100 |
||
| 981 | ) : false; |
||
| 982 | MediaWiki\restoreWarnings(); |
||
| 983 | |||
| 984 | $invokedWithSuccess = true; |
||
| 985 | if ( $sock ) { |
||
| 986 | $special = SpecialPageFactory::getPage( 'RunJobs' ); |
||
| 987 | $url = $special->getPageTitle()->getCanonicalURL( $query ); |
||
| 988 | $req = ( |
||
| 989 | "POST $url HTTP/1.1\r\n" . |
||
| 990 | "Host: {$info['host']}\r\n" . |
||
| 991 | "Connection: Close\r\n" . |
||
| 992 | "Content-Length: 0\r\n\r\n" |
||
| 993 | ); |
||
| 994 | |||
| 995 | $runJobsLogger->info( "Running $n job(s) via '$url'" ); |
||
| 996 | // Send a cron API request to be performed in the background. |
||
| 997 | // Give up if this takes too long to send (which should be rare). |
||
| 998 | stream_set_timeout( $sock, 2 ); |
||
| 999 | $bytes = fwrite( $sock, $req ); |
||
| 1000 | if ( $bytes !== strlen( $req ) ) { |
||
| 1001 | $invokedWithSuccess = false; |
||
| 1002 | $runJobsLogger->error( "Failed to start cron API (socket write error)" ); |
||
| 1003 | } else { |
||
| 1004 | // Do not wait for the response (the script should handle client aborts). |
||
| 1005 | // Make sure that we don't close before that script reaches ignore_user_abort(). |
||
| 1006 | $start = microtime( true ); |
||
| 1007 | $status = fgets( $sock ); |
||
| 1008 | $sec = microtime( true ) - $start; |
||
| 1009 | if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) { |
||
| 1010 | $invokedWithSuccess = false; |
||
| 1011 | $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" ); |
||
| 1012 | } |
||
| 1013 | } |
||
| 1014 | fclose( $sock ); |
||
| 1015 | } else { |
||
| 1016 | $invokedWithSuccess = false; |
||
| 1017 | $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); |
||
| 1018 | } |
||
| 1019 | |||
| 1020 | // Fall back to running the job(s) while the user waits if needed |
||
| 1021 | if ( !$invokedWithSuccess ) { |
||
| 1022 | $runJobsLogger->warning( "Jobs switched to blocking; Special:RunJobs disabled" ); |
||
| 1023 | |||
| 1024 | $runner = new JobRunner( $runJobsLogger ); |
||
| 1025 | $runner->run( [ 'maxJobs' => $n ] ); |
||
| 1026 | } |
||
| 1027 | } |
||
| 1028 | } |
||
| 1029 |
Let’s take a look at an example:
In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.
Available Fixes
Change the type-hint for the parameter:
Add an additional type-check:
Add the method to the interface: