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 | * - Normalise non-standard title urls: |
||
317 | * /w/index.php?title=Foo_Bar -> /wiki/Foo_Bar |
||
318 | * - Don't redirect anything with query parameters other than 'title' or 'action=view'. |
||
319 | * |
||
320 | * @param Title $title |
||
321 | * @return bool True if a redirect was set. |
||
322 | * @throws HttpError |
||
323 | */ |
||
324 | private function tryNormaliseRedirect( Title $title ) { |
||
325 | $request = $this->context->getRequest(); |
||
326 | $output = $this->context->getOutput(); |
||
327 | |||
328 | if ( $request->getVal( 'action', 'view' ) != 'view' |
||
329 | || $request->wasPosted() |
||
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 | |||
345 | if ( $targetUrl != $request->getFullRequestURL() ) { |
||
346 | $output->setCdnMaxage( 1200 ); |
||
347 | $output->redirect( $targetUrl, '301' ); |
||
348 | return true; |
||
349 | } |
||
350 | |||
351 | // If there is no title, or the title is in a non-standard encoding, we demand |
||
352 | // a redirect. If cgi somehow changed the 'title' query to be non-standard while |
||
353 | // the url is standard, the server is misconfigured. |
||
354 | if ( $request->getVal( 'title' ) === null |
||
355 | || $title->getPrefixedDBkey() != $request->getVal( 'title' ) |
||
356 | ) { |
||
357 | $message = "Redirect loop detected!\n\n" . |
||
358 | "This means the wiki got confused about what page was " . |
||
359 | "requested; this sometimes happens when moving a wiki " . |
||
360 | "to a new server or changing the server configuration.\n\n"; |
||
361 | |||
362 | if ( $this->config->get( 'UsePathInfo' ) ) { |
||
363 | $message .= "The wiki is trying to interpret the page " . |
||
364 | "title from the URL path portion (PATH_INFO), which " . |
||
365 | "sometimes fails depending on the web server. Try " . |
||
366 | "setting \"\$wgUsePathInfo = false;\" in your " . |
||
367 | "LocalSettings.php, or check that \$wgArticlePath " . |
||
368 | "is correct."; |
||
369 | } else { |
||
370 | $message .= "Your web server was detected as possibly not " . |
||
371 | "supporting URL path components (PATH_INFO) correctly; " . |
||
372 | "check your LocalSettings.php for a customized " . |
||
373 | "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . |
||
374 | "to true."; |
||
375 | } |
||
376 | throw new HttpError( 500, $message ); |
||
377 | } |
||
378 | return false; |
||
379 | } |
||
380 | |||
381 | /** |
||
382 | * Initialize the main Article object for "standard" actions (view, etc) |
||
383 | * Create an Article object for the page, following redirects if needed. |
||
384 | * |
||
385 | * @return Article|string An Article, or a string to redirect to another URL |
||
386 | */ |
||
387 | private function initializeArticle() { |
||
388 | $title = $this->context->getTitle(); |
||
389 | if ( $this->context->canUseWikiPage() ) { |
||
390 | // Try to use request context wiki page, as there |
||
391 | // is already data from db saved in per process |
||
392 | // cache there from this->getAction() call. |
||
393 | $page = $this->context->getWikiPage(); |
||
394 | } else { |
||
395 | // This case should not happen, but just in case. |
||
396 | // @TODO: remove this or use an exception |
||
397 | $page = WikiPage::factory( $title ); |
||
398 | $this->context->setWikiPage( $page ); |
||
0 ignored issues
–
show
|
|||
399 | wfWarn( "RequestContext::canUseWikiPage() returned false" ); |
||
400 | } |
||
401 | |||
402 | // Make GUI wrapper for the WikiPage |
||
403 | $article = Article::newFromWikiPage( $page, $this->context ); |
||
404 | |||
405 | // Skip some unnecessary code if the content model doesn't support redirects |
||
406 | if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) { |
||
407 | return $article; |
||
408 | } |
||
409 | |||
410 | $request = $this->context->getRequest(); |
||
411 | |||
412 | // Namespace might change when using redirects |
||
413 | // Check for redirects ... |
||
414 | $action = $request->getVal( 'action', 'view' ); |
||
415 | $file = ( $page instanceof WikiFilePage ) ? $page->getFile() : null; |
||
416 | if ( ( $action == 'view' || $action == 'render' ) // ... for actions that show content |
||
417 | && !$request->getVal( 'oldid' ) // ... and are not old revisions |
||
418 | && !$request->getVal( 'diff' ) // ... and not when showing diff |
||
419 | && $request->getVal( 'redirect' ) != 'no' // ... unless explicitly told not to |
||
420 | // ... and the article is not a non-redirect image page with associated file |
||
421 | && !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) |
||
422 | ) { |
||
423 | // Give extensions a change to ignore/handle redirects as needed |
||
424 | $ignoreRedirect = $target = false; |
||
425 | |||
426 | Hooks::run( 'InitializeArticleMaybeRedirect', |
||
427 | [ &$title, &$request, &$ignoreRedirect, &$target, &$article ] ); |
||
428 | $page = $article->getPage(); // reflect any hook changes |
||
429 | |||
430 | // Follow redirects only for... redirects. |
||
431 | // If $target is set, then a hook wanted to redirect. |
||
432 | if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) { |
||
433 | // Is the target already set by an extension? |
||
434 | $target = $target ? $target : $page->followRedirect(); |
||
435 | if ( is_string( $target ) ) { |
||
436 | if ( !$this->config->get( 'DisableHardRedirects' ) ) { |
||
437 | // we'll need to redirect |
||
438 | return $target; |
||
439 | } |
||
440 | } |
||
441 | if ( is_object( $target ) ) { |
||
442 | // Rewrite environment to redirected article |
||
443 | $rpage = WikiPage::factory( $target ); |
||
444 | $rpage->loadPageData(); |
||
445 | if ( $rpage->exists() || ( is_object( $file ) && !$file->isLocal() ) ) { |
||
446 | $rarticle = Article::newFromWikiPage( $rpage, $this->context ); |
||
447 | $rarticle->setRedirectedFrom( $title ); |
||
448 | |||
449 | $article = $rarticle; |
||
450 | $this->context->setTitle( $target ); |
||
451 | $this->context->setWikiPage( $article->getPage() ); |
||
452 | } |
||
453 | } |
||
454 | } else { |
||
455 | // Article may have been changed by hook |
||
456 | $this->context->setTitle( $article->getTitle() ); |
||
457 | $this->context->setWikiPage( $article->getPage() ); |
||
458 | } |
||
459 | } |
||
460 | |||
461 | return $article; |
||
462 | } |
||
463 | |||
464 | /** |
||
465 | * Perform one of the "standard" actions |
||
466 | * |
||
467 | * @param Page $page |
||
468 | * @param Title $requestTitle The original title, before any redirects were applied |
||
469 | */ |
||
470 | private function performAction( Page $page, Title $requestTitle ) { |
||
471 | $request = $this->context->getRequest(); |
||
472 | $output = $this->context->getOutput(); |
||
473 | $title = $this->context->getTitle(); |
||
474 | $user = $this->context->getUser(); |
||
475 | |||
476 | if ( !Hooks::run( 'MediaWikiPerformAction', |
||
477 | [ $output, $page, $title, $user, $request, $this ] ) |
||
478 | ) { |
||
479 | return; |
||
480 | } |
||
481 | |||
482 | $act = $this->getAction(); |
||
483 | $action = Action::factory( $act, $page, $this->context ); |
||
484 | |||
485 | if ( $action instanceof Action ) { |
||
486 | // Narrow DB query expectations for this HTTP request |
||
487 | $trxLimits = $this->config->get( 'TrxProfilerLimits' ); |
||
488 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
||
489 | if ( $request->wasPosted() && !$action->doesWrites() ) { |
||
490 | $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ ); |
||
491 | $request->markAsSafeRequest(); |
||
492 | } |
||
493 | |||
494 | # Let CDN cache things if we can purge them. |
||
495 | if ( $this->config->get( 'UseSquid' ) && |
||
496 | in_array( |
||
497 | // Use PROTO_INTERNAL because that's what getCdnUrls() uses |
||
498 | wfExpandUrl( $request->getRequestURL(), PROTO_INTERNAL ), |
||
499 | $requestTitle->getCdnUrls() |
||
500 | ) |
||
501 | ) { |
||
502 | $output->setCdnMaxage( $this->config->get( 'SquidMaxage' ) ); |
||
503 | } |
||
504 | |||
505 | $action->show(); |
||
506 | return; |
||
507 | } |
||
508 | |||
509 | if ( Hooks::run( 'UnknownAction', [ $request->getVal( 'action', 'view' ), $page ] ) ) { |
||
510 | $output->setStatusCode( 404 ); |
||
511 | $output->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); |
||
512 | } |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * Run the current MediaWiki instance; index.php just calls this |
||
517 | */ |
||
518 | public function run() { |
||
519 | try { |
||
520 | $this->setDBProfilingAgent(); |
||
521 | try { |
||
522 | $this->main(); |
||
523 | } catch ( ErrorPageError $e ) { |
||
524 | // Bug 62091: while exceptions are convenient to bubble up GUI errors, |
||
525 | // they are not internal application faults. As with normal requests, this |
||
526 | // should commit, print the output, do deferred updates, jobs, and profiling. |
||
527 | $this->doPreOutputCommit(); |
||
528 | $e->report(); // display the GUI error |
||
529 | } |
||
530 | } catch ( Exception $e ) { |
||
531 | $context = $this->context; |
||
532 | $action = $context->getRequest()->getVal( 'action', 'view' ); |
||
533 | if ( |
||
534 | $e instanceof DBConnectionError && |
||
535 | $context->hasTitle() && |
||
536 | $context->getTitle()->canExist() && |
||
537 | in_array( $action, [ 'view', 'history' ], true ) && |
||
538 | HTMLFileCache::useFileCache( $this->context, HTMLFileCache::MODE_OUTAGE ) |
||
539 | ) { |
||
540 | // Try to use any (even stale) file during outages... |
||
541 | $cache = new HTMLFileCache( $context->getTitle(), 'view' ); |
||
542 | if ( $cache->isCached() ) { |
||
543 | $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE ); |
||
544 | print MWExceptionRenderer::getHTML( $e ); |
||
545 | exit; |
||
546 | } |
||
547 | |||
548 | } |
||
549 | |||
550 | MWExceptionHandler::handleException( $e ); |
||
551 | } |
||
552 | |||
553 | $this->doPostOutputShutdown( 'normal' ); |
||
554 | } |
||
555 | |||
556 | private function setDBProfilingAgent() { |
||
557 | $services = MediaWikiServices::getInstance(); |
||
558 | // Add a comment for easy SHOW PROCESSLIST interpretation |
||
559 | $name = $this->context->getUser()->getName(); |
||
560 | $services->getDBLoadBalancerFactory()->setAgentName( |
||
561 | mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) . '...' : $name |
||
562 | ); |
||
563 | } |
||
564 | |||
565 | /** |
||
566 | * @see MediaWiki::preOutputCommit() |
||
567 | * @param callable $postCommitWork [default: null] |
||
568 | * @since 1.26 |
||
569 | */ |
||
570 | public function doPreOutputCommit( callable $postCommitWork = null ) { |
||
571 | self::preOutputCommit( $this->context, $postCommitWork ); |
||
572 | } |
||
573 | |||
574 | /** |
||
575 | * This function commits all DB changes as needed before |
||
576 | * the user can receive a response (in case commit fails) |
||
577 | * |
||
578 | * @param IContextSource $context |
||
579 | * @param callable $postCommitWork [default: null] |
||
580 | * @since 1.27 |
||
581 | */ |
||
582 | public static function preOutputCommit( |
||
583 | IContextSource $context, callable $postCommitWork = null |
||
584 | ) { |
||
585 | // Either all DBs should commit or none |
||
586 | ignore_user_abort( true ); |
||
587 | |||
588 | $config = $context->getConfig(); |
||
589 | $request = $context->getRequest(); |
||
590 | $output = $context->getOutput(); |
||
591 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
||
592 | |||
593 | // Commit all changes |
||
594 | $lbFactory->commitMasterChanges( |
||
595 | __METHOD__, |
||
596 | // Abort if any transaction was too big |
||
597 | [ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ] |
||
598 | ); |
||
599 | wfDebug( __METHOD__ . ': primary transaction round committed' ); |
||
600 | |||
601 | // Run updates that need to block the user or affect output (this is the last chance) |
||
602 | DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND ); |
||
603 | wfDebug( __METHOD__ . ': pre-send deferred updates completed' ); |
||
604 | |||
605 | // Decide when clients block on ChronologyProtector DB position writes |
||
606 | $urlDomainDistance = ( |
||
607 | $request->wasPosted() && |
||
608 | $output->getRedirect() && |
||
609 | $lbFactory->hasOrMadeRecentMasterChanges( INF ) |
||
610 | ) ? self::getUrlDomainDistance( $output->getRedirect(), $context ) : false; |
||
611 | |||
612 | if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) { |
||
613 | // OutputPage::output() will be fast; $postCommitWork will not be useful for |
||
614 | // masking the latency of syncing DB positions accross all datacenters synchronously. |
||
615 | // Instead, make use of the RTT time of the client follow redirects. |
||
616 | $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC; |
||
617 | $cpPosTime = microtime( true ); |
||
618 | // Client's next request should see 1+ positions with this DBMasterPos::asOf() time |
||
619 | if ( $urlDomainDistance === 'local' ) { |
||
620 | // Client will stay on this domain, so set an unobtrusive cookie |
||
621 | $expires = time() + ChronologyProtector::POSITION_TTL; |
||
622 | $options = [ 'prefix' => '' ]; |
||
623 | $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options ); |
||
624 | } else { |
||
625 | // Cookies may not work across wiki domains, so use a URL parameter |
||
626 | $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery( |
||
627 | $output->getRedirect(), |
||
628 | $cpPosTime |
||
629 | ); |
||
630 | $output->redirect( $safeUrl ); |
||
631 | } |
||
632 | } else { |
||
633 | // OutputPage::output() is fairly slow; run it in $postCommitWork to mask |
||
634 | // the latency of syncing DB positions accross all datacenters synchronously |
||
635 | $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC; |
||
636 | if ( $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) { |
||
637 | $cpPosTime = microtime( true ); |
||
638 | // Set a cookie in case the DB position store cannot sync accross datacenters. |
||
639 | // This will at least cover the common case of the user staying on the domain. |
||
640 | $expires = time() + ChronologyProtector::POSITION_TTL; |
||
641 | $options = [ 'prefix' => '' ]; |
||
642 | $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options ); |
||
643 | } |
||
644 | } |
||
645 | // Record ChronologyProtector positions for DBs affected in this request at this point |
||
646 | $lbFactory->shutdown( $flags, $postCommitWork ); |
||
647 | wfDebug( __METHOD__ . ': LBFactory shutdown completed' ); |
||
648 | |||
649 | // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this |
||
650 | // POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so |
||
651 | // ChronologyProtector works for cacheable URLs. |
||
652 | if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) { |
||
653 | $expires = time() + $config->get( 'DataCenterUpdateStickTTL' ); |
||
654 | $options = [ 'prefix' => '' ]; |
||
655 | $request->response()->setCookie( 'UseDC', 'master', $expires, $options ); |
||
656 | $request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options ); |
||
657 | } |
||
658 | |||
659 | // Avoid letting a few seconds of replica DB lag cause a month of stale data. This logic is |
||
660 | // also intimately related to the value of $wgCdnReboundPurgeDelay. |
||
661 | if ( $lbFactory->laggedReplicaUsed() ) { |
||
662 | $maxAge = $config->get( 'CdnMaxageLagged' ); |
||
663 | $output->lowerCdnMaxage( $maxAge ); |
||
664 | $request->response()->header( "X-Database-Lagged: true" ); |
||
665 | wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" ); |
||
666 | } |
||
667 | |||
668 | // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069) |
||
669 | if ( MessageCache::singleton()->isDisabled() ) { |
||
670 | $maxAge = $config->get( 'CdnMaxageSubstitute' ); |
||
671 | $output->lowerCdnMaxage( $maxAge ); |
||
672 | $request->response()->header( "X-Response-Substitute: true" ); |
||
673 | } |
||
674 | } |
||
675 | |||
676 | /** |
||
677 | * @param string $url |
||
678 | * @param IContextSource $context |
||
679 | * @return string Either "local", "remote" if in the farm, "external" otherwise |
||
680 | */ |
||
681 | private static function getUrlDomainDistance( $url, IContextSource $context ) { |
||
682 | static $relevantKeys = [ 'host' => true, 'port' => true ]; |
||
683 | |||
684 | $infoCandidate = wfParseUrl( $url ); |
||
685 | if ( $infoCandidate === false ) { |
||
686 | return 'external'; |
||
687 | } |
||
688 | |||
689 | $infoCandidate = array_intersect_key( $infoCandidate, $relevantKeys ); |
||
690 | $clusterHosts = array_merge( |
||
691 | // Local wiki host (the most common case) |
||
692 | [ $context->getConfig()->get( 'CanonicalServer' ) ], |
||
693 | // Any local/remote wiki virtual hosts for this wiki farm |
||
694 | $context->getConfig()->get( 'LocalVirtualHosts' ) |
||
695 | ); |
||
696 | |||
697 | foreach ( $clusterHosts as $i => $clusterHost ) { |
||
698 | $parseUrl = wfParseUrl( $clusterHost ); |
||
699 | if ( !$parseUrl ) { |
||
700 | continue; |
||
701 | } |
||
702 | $infoHost = array_intersect_key( $parseUrl, $relevantKeys ); |
||
703 | if ( $infoCandidate === $infoHost ) { |
||
704 | return ( $i === 0 ) ? 'local' : 'remote'; |
||
705 | } |
||
706 | } |
||
707 | |||
708 | return 'external'; |
||
709 | } |
||
710 | |||
711 | /** |
||
712 | * This function does work that can be done *after* the |
||
713 | * user gets the HTTP response so they don't block on it |
||
714 | * |
||
715 | * This manages deferred updates, job insertion, |
||
716 | * final commit, and the logging of profiling data |
||
717 | * |
||
718 | * @param string $mode Use 'fast' to always skip job running |
||
719 | * @since 1.26 |
||
720 | */ |
||
721 | public function doPostOutputShutdown( $mode = 'normal' ) { |
||
722 | $timing = $this->context->getTiming(); |
||
723 | $timing->mark( 'requestShutdown' ); |
||
724 | |||
725 | // Show visible profiling data if enabled (which cannot be post-send) |
||
726 | Profiler::instance()->logDataPageOutputOnly(); |
||
727 | |||
728 | $callback = function () use ( $mode ) { |
||
729 | try { |
||
730 | $this->restInPeace( $mode ); |
||
731 | } catch ( Exception $e ) { |
||
732 | MWExceptionHandler::handleException( $e ); |
||
733 | } |
||
734 | }; |
||
735 | |||
736 | // Defer everything else... |
||
737 | if ( function_exists( 'register_postsend_function' ) ) { |
||
738 | // https://github.com/facebook/hhvm/issues/1230 |
||
739 | register_postsend_function( $callback ); |
||
740 | } else { |
||
741 | if ( function_exists( 'fastcgi_finish_request' ) ) { |
||
742 | fastcgi_finish_request(); |
||
743 | } else { |
||
744 | // Either all DB and deferred updates should happen or none. |
||
745 | // The latter should not be cancelled due to client disconnect. |
||
746 | ignore_user_abort( true ); |
||
747 | } |
||
748 | |||
749 | $callback(); |
||
750 | } |
||
751 | } |
||
752 | |||
753 | private function main() { |
||
754 | global $wgTitle; |
||
755 | |||
756 | $output = $this->context->getOutput(); |
||
757 | $request = $this->context->getRequest(); |
||
758 | |||
759 | // Send Ajax requests to the Ajax dispatcher. |
||
760 | if ( $this->config->get( 'UseAjax' ) && $request->getVal( 'action' ) === 'ajax' ) { |
||
761 | // Set a dummy title, because $wgTitle == null might break things |
||
762 | $title = Title::makeTitle( NS_SPECIAL, 'Badtitle/performing an AJAX call in ' |
||
763 | . __METHOD__ |
||
764 | ); |
||
765 | $this->context->setTitle( $title ); |
||
766 | $wgTitle = $title; |
||
767 | |||
768 | $dispatcher = new AjaxDispatcher( $this->config ); |
||
769 | $dispatcher->performAction( $this->context->getUser() ); |
||
770 | |||
771 | return; |
||
772 | } |
||
773 | |||
774 | // Get title from request parameters, |
||
775 | // is set on the fly by parseTitle the first time. |
||
776 | $title = $this->getTitle(); |
||
777 | $action = $this->getAction(); |
||
778 | $wgTitle = $title; |
||
779 | |||
780 | // Set DB query expectations for this HTTP request |
||
781 | $trxLimits = $this->config->get( 'TrxProfilerLimits' ); |
||
782 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
||
783 | $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) ); |
||
784 | if ( $request->hasSafeMethod() ) { |
||
785 | $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ ); |
||
786 | } else { |
||
787 | $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ ); |
||
788 | } |
||
789 | |||
790 | // If the user has forceHTTPS set to true, or if the user |
||
791 | // is in a group requiring HTTPS, or if they have the HTTPS |
||
792 | // preference set, redirect them to HTTPS. |
||
793 | // Note: Do this after $wgTitle is setup, otherwise the hooks run from |
||
794 | // isLoggedIn() will do all sorts of weird stuff. |
||
795 | if ( |
||
796 | $request->getProtocol() == 'http' && |
||
797 | // switch to HTTPS only when supported by the server |
||
798 | preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) && |
||
799 | ( |
||
800 | $request->getSession()->shouldForceHTTPS() || |
||
801 | // Check the cookie manually, for paranoia |
||
802 | $request->getCookie( 'forceHTTPS', '' ) || |
||
803 | // check for prefixed version that was used for a time in older MW versions |
||
804 | $request->getCookie( 'forceHTTPS' ) || |
||
805 | // Avoid checking the user and groups unless it's enabled. |
||
806 | ( |
||
807 | $this->context->getUser()->isLoggedIn() |
||
808 | && $this->context->getUser()->requiresHTTPS() |
||
809 | ) |
||
810 | ) |
||
811 | ) { |
||
812 | $oldUrl = $request->getFullRequestURL(); |
||
813 | $redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl ); |
||
814 | |||
815 | // ATTENTION: This hook is likely to be removed soon due to overall design of the system. |
||
816 | if ( Hooks::run( 'BeforeHttpsRedirect', [ $this->context, &$redirUrl ] ) ) { |
||
817 | |||
818 | if ( $request->wasPosted() ) { |
||
819 | // This is weird and we'd hope it almost never happens. This |
||
820 | // means that a POST came in via HTTP and policy requires us |
||
821 | // redirecting to HTTPS. It's likely such a request is going |
||
822 | // to fail due to post data being lost, but let's try anyway |
||
823 | // and just log the instance. |
||
824 | |||
825 | // @todo FIXME: See if we could issue a 307 or 308 here, need |
||
826 | // to see how clients (automated & browser) behave when we do |
||
827 | wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" ); |
||
828 | } |
||
829 | // Setup dummy Title, otherwise OutputPage::redirect will fail |
||
830 | $title = Title::newFromText( 'REDIR', NS_MAIN ); |
||
831 | $this->context->setTitle( $title ); |
||
832 | // Since we only do this redir to change proto, always send a vary header |
||
833 | $output->addVaryHeader( 'X-Forwarded-Proto' ); |
||
834 | $output->redirect( $redirUrl ); |
||
835 | $output->output(); |
||
836 | |||
837 | return; |
||
838 | } |
||
839 | } |
||
840 | |||
841 | if ( $title->canExist() && HTMLFileCache::useFileCache( $this->context ) ) { |
||
842 | // Try low-level file cache hit |
||
843 | $cache = new HTMLFileCache( $title, $action ); |
||
844 | if ( $cache->isCacheGood( /* Assume up to date */ ) ) { |
||
845 | // Check incoming headers to see if client has this cached |
||
846 | $timestamp = $cache->cacheTimestamp(); |
||
847 | if ( !$output->checkLastModified( $timestamp ) ) { |
||
848 | $cache->loadFromFileCache( $this->context ); |
||
849 | } |
||
850 | // Do any stats increment/watchlist stuff, assuming user is viewing the |
||
851 | // latest revision (which should always be the case for file cache) |
||
852 | $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() ); |
||
853 | // Tell OutputPage that output is taken care of |
||
854 | $output->disable(); |
||
855 | |||
856 | return; |
||
857 | } |
||
858 | } |
||
859 | |||
860 | // Actually do the work of the request and build up any output |
||
861 | $this->performRequest(); |
||
862 | |||
863 | // GUI-ify and stash the page output in MediaWiki::doPreOutputCommit() while |
||
864 | // ChronologyProtector synchronizes DB positions or slaves accross all datacenters. |
||
865 | $buffer = null; |
||
866 | $outputWork = function () use ( $output, &$buffer ) { |
||
867 | if ( $buffer === null ) { |
||
868 | $buffer = $output->output( true ); |
||
869 | } |
||
870 | |||
871 | return $buffer; |
||
872 | }; |
||
873 | |||
874 | // Now commit any transactions, so that unreported errors after |
||
875 | // output() don't roll back the whole DB transaction and so that |
||
876 | // we avoid having both success and error text in the response |
||
877 | $this->doPreOutputCommit( $outputWork ); |
||
878 | |||
879 | // Now send the actual output |
||
880 | print $outputWork(); |
||
881 | } |
||
882 | |||
883 | /** |
||
884 | * Ends this task peacefully |
||
885 | * @param string $mode Use 'fast' to always skip job running |
||
886 | */ |
||
887 | public function restInPeace( $mode = 'fast' ) { |
||
888 | $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); |
||
889 | // Assure deferred updates are not in the main transaction |
||
890 | $lbFactory->commitMasterChanges( __METHOD__ ); |
||
891 | |||
892 | // Loosen DB query expectations since the HTTP client is unblocked |
||
893 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
||
894 | $trxProfiler->resetExpectations(); |
||
895 | $trxProfiler->setExpectations( |
||
896 | $this->config->get( 'TrxProfilerLimits' )['PostSend'], |
||
897 | __METHOD__ |
||
898 | ); |
||
899 | |||
900 | // Do any deferred jobs |
||
901 | DeferredUpdates::doUpdates( 'enqueue' ); |
||
902 | DeferredUpdates::setImmediateMode( true ); |
||
903 | |||
904 | // Make sure any lazy jobs are pushed |
||
905 | JobQueueGroup::pushLazyJobs(); |
||
906 | |||
907 | // Now that everything specific to this request is done, |
||
908 | // try to occasionally run jobs (if enabled) from the queues |
||
909 | if ( $mode === 'normal' ) { |
||
910 | $this->triggerJobs(); |
||
911 | } |
||
912 | |||
913 | // Log profiling data, e.g. in the database or UDP |
||
914 | wfLogProfilingData(); |
||
915 | |||
916 | // Commit and close up! |
||
917 | $lbFactory->commitMasterChanges( __METHOD__ ); |
||
918 | $lbFactory->shutdown( LBFactory::SHUTDOWN_NO_CHRONPROT ); |
||
919 | |||
920 | wfDebug( "Request ended normally\n" ); |
||
921 | } |
||
922 | |||
923 | /** |
||
924 | * Potentially open a socket and sent an HTTP request back to the server |
||
925 | * to run a specified number of jobs. This registers a callback to cleanup |
||
926 | * the socket once it's done. |
||
927 | */ |
||
928 | public function triggerJobs() { |
||
929 | $jobRunRate = $this->config->get( 'JobRunRate' ); |
||
930 | if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) { |
||
931 | return; // recursion guard |
||
932 | } elseif ( $jobRunRate <= 0 || wfReadOnly() ) { |
||
933 | return; |
||
934 | } |
||
935 | |||
936 | if ( $jobRunRate < 1 ) { |
||
937 | $max = mt_getrandmax(); |
||
938 | if ( mt_rand( 0, $max ) > $max * $jobRunRate ) { |
||
939 | return; // the higher the job run rate, the less likely we return here |
||
940 | } |
||
941 | $n = 1; |
||
942 | } else { |
||
943 | $n = intval( $jobRunRate ); |
||
944 | } |
||
945 | |||
946 | $runJobsLogger = LoggerFactory::getInstance( 'runJobs' ); |
||
947 | |||
948 | // Fall back to running the job(s) while the user waits if needed |
||
949 | if ( !$this->config->get( 'RunJobsAsync' ) ) { |
||
950 | $runner = new JobRunner( $runJobsLogger ); |
||
951 | $runner->run( [ 'maxJobs' => $n ] ); |
||
952 | return; |
||
953 | } |
||
954 | |||
955 | // Do not send request if there are probably no jobs |
||
956 | try { |
||
957 | $group = JobQueueGroup::singleton(); |
||
958 | if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) { |
||
959 | return; |
||
960 | } |
||
961 | } catch ( JobQueueError $e ) { |
||
962 | MWExceptionHandler::logException( $e ); |
||
963 | return; // do not make the site unavailable |
||
964 | } |
||
965 | |||
966 | $query = [ 'title' => 'Special:RunJobs', |
||
967 | 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ]; |
||
968 | $query['signature'] = SpecialRunJobs::getQuerySignature( |
||
969 | $query, $this->config->get( 'SecretKey' ) ); |
||
970 | |||
971 | $errno = $errstr = null; |
||
972 | $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) ); |
||
973 | $host = $info ? $info['host'] : null; |
||
974 | $port = 80; |
||
975 | if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) { |
||
976 | $host = "tls://" . $host; |
||
977 | $port = 443; |
||
978 | } |
||
979 | if ( isset( $info['port'] ) ) { |
||
980 | $port = $info['port']; |
||
981 | } |
||
982 | |||
983 | MediaWiki\suppressWarnings(); |
||
984 | $sock = $host ? fsockopen( |
||
985 | $host, |
||
986 | $port, |
||
987 | $errno, |
||
988 | $errstr, |
||
989 | // If it takes more than 100ms to connect to ourselves there is a problem... |
||
990 | 0.100 |
||
991 | ) : false; |
||
992 | MediaWiki\restoreWarnings(); |
||
993 | |||
994 | $invokedWithSuccess = true; |
||
995 | if ( $sock ) { |
||
996 | $special = SpecialPageFactory::getPage( 'RunJobs' ); |
||
997 | $url = $special->getPageTitle()->getCanonicalURL( $query ); |
||
998 | $req = ( |
||
999 | "POST $url HTTP/1.1\r\n" . |
||
1000 | "Host: {$info['host']}\r\n" . |
||
1001 | "Connection: Close\r\n" . |
||
1002 | "Content-Length: 0\r\n\r\n" |
||
1003 | ); |
||
1004 | |||
1005 | $runJobsLogger->info( "Running $n job(s) via '$url'" ); |
||
1006 | // Send a cron API request to be performed in the background. |
||
1007 | // Give up if this takes too long to send (which should be rare). |
||
1008 | stream_set_timeout( $sock, 2 ); |
||
1009 | $bytes = fwrite( $sock, $req ); |
||
1010 | if ( $bytes !== strlen( $req ) ) { |
||
1011 | $invokedWithSuccess = false; |
||
1012 | $runJobsLogger->error( "Failed to start cron API (socket write error)" ); |
||
1013 | } else { |
||
1014 | // Do not wait for the response (the script should handle client aborts). |
||
1015 | // Make sure that we don't close before that script reaches ignore_user_abort(). |
||
1016 | $start = microtime( true ); |
||
1017 | $status = fgets( $sock ); |
||
1018 | $sec = microtime( true ) - $start; |
||
1019 | if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) { |
||
1020 | $invokedWithSuccess = false; |
||
1021 | $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" ); |
||
1022 | } |
||
1023 | } |
||
1024 | fclose( $sock ); |
||
1025 | } else { |
||
1026 | $invokedWithSuccess = false; |
||
1027 | $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); |
||
1028 | } |
||
1029 | |||
1030 | // Fall back to running the job(s) while the user waits if needed |
||
1031 | if ( !$invokedWithSuccess ) { |
||
1032 | $runJobsLogger->warning( "Jobs switched to blocking; Special:RunJobs disabled" ); |
||
1033 | |||
1034 | $runner = new JobRunner( $runJobsLogger ); |
||
1035 | $runner->run( [ 'maxJobs' => $n ] ); |
||
1036 | } |
||
1037 | } |
||
1038 | } |
||
1039 |
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: