Completed
Branch master (dc3656)
by
unknown
30:14
created

MediaWiki::triggerJobs()   F

Complexity

Conditions 18
Paths 267

Size

Total Lines 110
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 77
c 1
b 0
f 0
nc 267
nop 0
dl 0
loc 110
rs 3.6714

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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() ) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method hasTitle() does only exist in the following implementations of said interface: RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
135
			try {
136
				$this->context->setTitle( $this->parseTitle() );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
137
			} catch ( MalformedTitleException $ex ) {
138
				$this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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' ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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 );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rdfrom of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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' ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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 );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setRequest() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
264
							// Do not varnish cache these. May vary even for anons
265
							$this->context->getOutput()->lowerCdnMaxage( 0 );
266
							$this->context->setTitle( $target );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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 );
0 ignored issues
show
Bug introduced by
It seems like $requestTitle defined by $title = $this->context->getTitle() on line 173 can be null; however, MediaWiki::performAction() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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' );
0 ignored issues
show
Security Bug introduced by
It seems like $targetUrl defined by wfExpandUrl($title->getFullURL(), PROTO_CURRENT) on line 343 can also be of type false; however, OutputPage::redirect() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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 );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->context->getTitle() on line 388 can be null; however, WikiPage::factory() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
398
			$this->context->setWikiPage( $page );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setWikiPage() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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() ) {
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->context->getTitle() on line 388 can be null; however, ContentHandler::getForTitle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getVal('oldid') of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
418
			&& !$request->getVal( 'diff' ) // ... and not when showing diff
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getVal('diff') of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->context->getTitle() on line 388 can be null; however, Article::setRedirectedFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
448
449
						$article = $rarticle;
450
						$this->context->setTitle( $target );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
451
						$this->context->setWikiPage( $article->getPage() );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setWikiPage() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
452
					}
453
				}
454
			} else {
455
				// Article may have been changed by hook
456
				$this->context->setTitle( $article->getTitle() );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
457
				$this->context->setWikiPage( $article->getPage() );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setWikiPage() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
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
			try {
521
				$this->main();
522
			} catch ( ErrorPageError $e ) {
523
				// Bug 62091: while exceptions are convenient to bubble up GUI errors,
524
				// they are not internal application faults. As with normal requests, this
525
				// should commit, print the output, do deferred updates, jobs, and profiling.
526
				$this->doPreOutputCommit();
527
				$e->report(); // display the GUI error
528
			}
529
		} catch ( Exception $e ) {
530
			MWExceptionHandler::handleException( $e );
531
		}
532
533
		$this->doPostOutputShutdown( 'normal' );
534
	}
535
536
	/**
537
	 * @see MediaWiki::preOutputCommit()
538
	 * @since 1.26
539
	 */
540
	public function doPreOutputCommit() {
541
		self::preOutputCommit( $this->context );
542
	}
543
544
	/**
545
	 * This function commits all DB changes as needed before
546
	 * the user can receive a response (in case commit fails)
547
	 *
548
	 * @param IContextSource $context
549
	 * @since 1.27
550
	 */
551
	public static function preOutputCommit( IContextSource $context ) {
552
		// Either all DBs should commit or none
553
		ignore_user_abort( true );
554
555
		$config = $context->getConfig();
556
557
		$factory = wfGetLBFactory();
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLBFactory() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
558
		// Commit all changes
559
		$factory->commitMasterChanges(
560
			__METHOD__,
561
			// Abort if any transaction was too big
562
			[ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ]
563
		);
564
565
		DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
566
		wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
567
568
		// Record ChronologyProtector positions
569
		$factory->shutdown();
570
		wfDebug( __METHOD__ . ': all transactions committed' );
571
572
		// Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
573
		// POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
574
		// ChronologyProtector works for cacheable URLs.
575
		$request = $context->getRequest();
576
		if ( $request->wasPosted() && $factory->hasOrMadeRecentMasterChanges() ) {
577
			$expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
578
			$options = [ 'prefix' => '' ];
579
			$request->response()->setCookie( 'UseDC', 'master', $expires, $options );
580
			$request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
581
		}
582
583
		// Avoid letting a few seconds of slave lag cause a month of stale data. This logic is
584
		// also intimately related to the value of $wgCdnReboundPurgeDelay.
585
		if ( $factory->laggedSlaveUsed() ) {
586
			$maxAge = $config->get( 'CdnMaxageLagged' );
587
			$context->getOutput()->lowerCdnMaxage( $maxAge );
588
			$request->response()->header( "X-Database-Lagged: true" );
589
			wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
590
		}
591
592
		// Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
593
		if ( MessageCache::singleton()->isDisabled() ) {
594
			$maxAge = $config->get( 'CdnMaxageSubstitute' );
595
			$context->getOutput()->lowerCdnMaxage( $maxAge );
596
			$request->response()->header( "X-Response-Substitute: true" );
597
		}
598
	}
599
600
	/**
601
	 * This function does work that can be done *after* the
602
	 * user gets the HTTP response so they don't block on it
603
	 *
604
	 * This manages deferred updates, job insertion,
605
	 * final commit, and the logging of profiling data
606
	 *
607
	 * @param string $mode Use 'fast' to always skip job running
608
	 * @since 1.26
609
	 */
610
	public function doPostOutputShutdown( $mode = 'normal' ) {
611
		$timing = $this->context->getTiming();
612
		$timing->mark( 'requestShutdown' );
613
614
		// Show visible profiling data if enabled (which cannot be post-send)
615
		Profiler::instance()->logDataPageOutputOnly();
616
617
		$that = $this;
618
		$callback = function () use ( $that, $mode ) {
619
			try {
620
				$that->restInPeace( $mode );
621
			} catch ( Exception $e ) {
622
				MWExceptionHandler::handleException( $e );
623
			}
624
		};
625
626
		// Defer everything else...
627
		if ( function_exists( 'register_postsend_function' ) ) {
628
			// https://github.com/facebook/hhvm/issues/1230
629
			register_postsend_function( $callback );
630
		} else {
631
			if ( function_exists( 'fastcgi_finish_request' ) ) {
632
				fastcgi_finish_request();
633
			} else {
634
				// Either all DB and deferred updates should happen or none.
635
				// The later should not be cancelled due to client disconnect.
636
				ignore_user_abort( true );
637
			}
638
639
			$callback();
640
		}
641
	}
642
643
	private function main() {
644
		global $wgTitle;
645
646
		$request = $this->context->getRequest();
647
648
		// Send Ajax requests to the Ajax dispatcher.
649
		if ( $this->config->get( 'UseAjax' ) && $request->getVal( 'action' ) === 'ajax' ) {
650
			// Set a dummy title, because $wgTitle == null might break things
651
			$title = Title::makeTitle( NS_SPECIAL, 'Badtitle/performing an AJAX call in '
652
				. __METHOD__
653
			);
654
			$this->context->setTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
655
			$wgTitle = $title;
656
657
			$dispatcher = new AjaxDispatcher( $this->config );
658
			$dispatcher->performAction( $this->context->getUser() );
659
			return;
660
		}
661
662
		// Get title from request parameters,
663
		// is set on the fly by parseTitle the first time.
664
		$title = $this->getTitle();
665
		$action = $this->getAction();
666
		$wgTitle = $title;
667
668
		// Set DB query expectations for this HTTP request
669
		$trxLimits = $this->config->get( 'TrxProfilerLimits' );
670
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
671
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
672
		if ( $request->hasSafeMethod() ) {
673
			$trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
674
		} else {
675
			$trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
676
		}
677
678
		// If the user has forceHTTPS set to true, or if the user
679
		// is in a group requiring HTTPS, or if they have the HTTPS
680
		// preference set, redirect them to HTTPS.
681
		// Note: Do this after $wgTitle is setup, otherwise the hooks run from
682
		// isLoggedIn() will do all sorts of weird stuff.
683
		if (
684
			$request->getProtocol() == 'http' &&
685
			// switch to HTTPS only when supported by the server
686
			preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) &&
687
			(
688
				$request->getSession()->shouldForceHTTPS() ||
689
				// Check the cookie manually, for paranoia
690
				$request->getCookie( 'forceHTTPS', '' ) ||
691
				// check for prefixed version that was used for a time in older MW versions
692
				$request->getCookie( 'forceHTTPS' ) ||
693
				// Avoid checking the user and groups unless it's enabled.
694
				(
695
					$this->context->getUser()->isLoggedIn()
696
					&& $this->context->getUser()->requiresHTTPS()
697
				)
698
			)
699
		) {
700
			$oldUrl = $request->getFullRequestURL();
701
			$redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
702
703
			// ATTENTION: This hook is likely to be removed soon due to overall design of the system.
704
			if ( Hooks::run( 'BeforeHttpsRedirect', [ $this->context, &$redirUrl ] ) ) {
705
706
				if ( $request->wasPosted() ) {
707
					// This is weird and we'd hope it almost never happens. This
708
					// means that a POST came in via HTTP and policy requires us
709
					// redirecting to HTTPS. It's likely such a request is going
710
					// to fail due to post data being lost, but let's try anyway
711
					// and just log the instance.
712
713
					// @todo FIXME: See if we could issue a 307 or 308 here, need
714
					// to see how clients (automated & browser) behave when we do
715
					wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
716
				}
717
				// Setup dummy Title, otherwise OutputPage::redirect will fail
718
				$title = Title::newFromText( 'REDIR', NS_MAIN );
719
				$this->context->setTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

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

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
720
				$output = $this->context->getOutput();
721
				// Since we only do this redir to change proto, always send a vary header
722
				$output->addVaryHeader( 'X-Forwarded-Proto' );
723
				$output->redirect( $redirUrl );
724
				$output->output();
725
				return;
726
			}
727
		}
728
729
		if ( $this->config->get( 'UseFileCache' ) && $title->getNamespace() >= 0 ) {
730
			if ( HTMLFileCache::useFileCache( $this->context ) ) {
731
				// Try low-level file cache hit
732
				$cache = new HTMLFileCache( $title, $action );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 664 can be null; however, HTMLFileCache::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
733
				if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
734
					// Check incoming headers to see if client has this cached
735
					$timestamp = $cache->cacheTimestamp();
736
					if ( !$this->context->getOutput()->checkLastModified( $timestamp ) ) {
0 ignored issues
show
Security Bug introduced by
It seems like $timestamp defined by $cache->cacheTimestamp() on line 735 can also be of type false; however, OutputPage::checkLastModified() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
737
						$cache->loadFromFileCache( $this->context );
738
					}
739
					// Do any stats increment/watchlist stuff
740
					// Assume we're viewing the latest revision (this should always be the case with file cache)
741
					$this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
742
					// Tell OutputPage that output is taken care of
743
					$this->context->getOutput()->disable();
744
					return;
745
				}
746
			}
747
		}
748
749
		// Actually do the work of the request and build up any output
750
		$this->performRequest();
751
752
		// Now commit any transactions, so that unreported errors after
753
		// output() don't roll back the whole DB transaction and so that
754
		// we avoid having both success and error text in the response
755
		$this->doPreOutputCommit();
756
757
		// Output everything!
758
		$this->context->getOutput()->output();
759
	}
760
761
	/**
762
	 * Ends this task peacefully
763
	 * @param string $mode Use 'fast' to always skip job running
764
	 */
765
	public function restInPeace( $mode = 'fast' ) {
766
		$factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
767
		// Assure deferred updates are not in the main transaction
768
		$factory->commitMasterChanges( __METHOD__ );
769
770
		// Loosen DB query expectations since the HTTP client is unblocked
771
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
772
		$trxProfiler->resetExpectations();
773
		$trxProfiler->setExpectations(
774
			$this->config->get( 'TrxProfilerLimits' )['PostSend'],
775
			__METHOD__
776
		);
777
778
		// Do any deferred jobs
779
		DeferredUpdates::doUpdates( 'enqueue' );
780
781
		// Make sure any lazy jobs are pushed
782
		JobQueueGroup::pushLazyJobs();
783
784
		// Now that everything specific to this request is done,
785
		// try to occasionally run jobs (if enabled) from the queues
786
		if ( $mode === 'normal' ) {
787
			$this->triggerJobs();
788
		}
789
790
		// Log profiling data, e.g. in the database or UDP
791
		wfLogProfilingData();
792
793
		// Commit and close up!
794
		$factory->commitMasterChanges( __METHOD__ );
795
		$factory->shutdown( LBFactory::SHUTDOWN_NO_CHRONPROT );
796
797
		wfDebug( "Request ended normally\n" );
798
	}
799
800
	/**
801
	 * Potentially open a socket and sent an HTTP request back to the server
802
	 * to run a specified number of jobs. This registers a callback to cleanup
803
	 * the socket once it's done.
804
	 */
805
	public function triggerJobs() {
806
		$jobRunRate = $this->config->get( 'JobRunRate' );
807
		if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
808
			return; // recursion guard
809
		} elseif ( $jobRunRate <= 0 || wfReadOnly() ) {
810
			return;
811
		}
812
813
		if ( $jobRunRate < 1 ) {
814
			$max = mt_getrandmax();
815
			if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
816
				return; // the higher the job run rate, the less likely we return here
817
			}
818
			$n = 1;
819
		} else {
820
			$n = intval( $jobRunRate );
821
		}
822
823
		$runJobsLogger = LoggerFactory::getInstance( 'runJobs' );
824
825
		// Fall back to running the job(s) while the user waits if needed
826
		if ( !$this->config->get( 'RunJobsAsync' ) ) {
827
			$runner = new JobRunner( $runJobsLogger );
828
			$runner->run( [ 'maxJobs' => $n ] );
829
			return;
830
		}
831
832
		// Do not send request if there are probably no jobs
833
		try {
834
			$group = JobQueueGroup::singleton();
835
			if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
836
				return;
837
			}
838
		} catch ( JobQueueError $e ) {
839
			MWExceptionHandler::logException( $e );
840
			return; // do not make the site unavailable
841
		}
842
843
		$query = [ 'title' => 'Special:RunJobs',
844
			'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ];
845
		$query['signature'] = SpecialRunJobs::getQuerySignature(
846
			$query, $this->config->get( 'SecretKey' ) );
847
848
		$errno = $errstr = null;
849
		$info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
850
		$host = $info ? $info['host'] : null;
851
		$port = 80;
852
		if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) {
853
			$host = "tls://" . $host;
854
			$port = 443;
855
		}
856
		if ( isset( $info['port'] ) ) {
857
			$port = $info['port'];
858
		}
859
860
		MediaWiki\suppressWarnings();
861
		$sock = $host ? fsockopen(
862
			$host,
863
			$port,
864
			$errno,
865
			$errstr,
866
			// If it takes more than 100ms to connect to ourselves there is a problem...
867
			0.100
868
		) : false;
869
		MediaWiki\restoreWarnings();
870
871
		$invokedWithSuccess = true;
872
		if ( $sock ) {
873
			$special = SpecialPageFactory::getPage( 'RunJobs' );
874
			$url = $special->getPageTitle()->getCanonicalURL( $query );
875
			$req = (
876
				"POST $url HTTP/1.1\r\n" .
877
				"Host: {$info['host']}\r\n" .
878
				"Connection: Close\r\n" .
879
				"Content-Length: 0\r\n\r\n"
880
			);
881
882
			$runJobsLogger->info( "Running $n job(s) via '$url'" );
883
			// Send a cron API request to be performed in the background.
884
			// Give up if this takes too long to send (which should be rare).
885
			stream_set_timeout( $sock, 2 );
886
			$bytes = fwrite( $sock, $req );
887
			if ( $bytes !== strlen( $req ) ) {
888
				$invokedWithSuccess = false;
889
				$runJobsLogger->error( "Failed to start cron API (socket write error)" );
890
			} else {
891
				// Do not wait for the response (the script should handle client aborts).
892
				// Make sure that we don't close before that script reaches ignore_user_abort().
893
				$start = microtime( true );
894
				$status = fgets( $sock );
895
				$sec = microtime( true ) - $start;
896
				if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
897
					$invokedWithSuccess = false;
898
					$runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" );
899
				}
900
			}
901
			fclose( $sock );
902
		} else {
903
			$invokedWithSuccess = false;
904
			$runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" );
905
		}
906
907
		// Fall back to running the job(s) while the user waits if needed
908
		if ( !$invokedWithSuccess ) {
909
			$runJobsLogger->warning( "Jobs switched to blocking; Special:RunJobs disabled" );
910
911
			$runner = new JobRunner( $runJobsLogger );
912
			$runner->run( [ 'maxJobs'  => $n ] );
913
		}
914
	}
915
}
916