Completed
Branch master (8d5465)
by
unknown
31:25
created

MediaWiki::isWikiClusterURL()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 2
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
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
	 * @param callable $postCommitWork [default: null]
539
	 * @since 1.26
540
	 */
541
	public function doPreOutputCommit( callable $postCommitWork = null ) {
542
		self::preOutputCommit( $this->context, $postCommitWork );
543
	}
544
545
	/**
546
	 * This function commits all DB changes as needed before
547
	 * the user can receive a response (in case commit fails)
548
	 *
549
	 * @param IContextSource $context
550
	 * @param callable $postCommitWork [default: null]
551
	 * @since 1.27
552
	 */
553
	public static function preOutputCommit(
554
		IContextSource $context, callable $postCommitWork = null
555
	) {
556
		// Either all DBs should commit or none
557
		ignore_user_abort( true );
558
559
		$config = $context->getConfig();
560
		$request = $context->getRequest();
561
		$output = $context->getOutput();
562
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
563
564
		// Commit all changes
565
		$lbFactory->commitMasterChanges(
566
			__METHOD__,
567
			// Abort if any transaction was too big
568
			[ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ]
569
		);
570
		wfDebug( __METHOD__ . ': primary transaction round committed' );
571
572
		// Run updates that need to block the user or affect output (this is the last chance)
573
		DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
574
		wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
575
576
		// Decide when clients block on ChronologyProtector DB position writes
577
		if (
578
			$request->wasPosted() &&
579
			$output->getRedirect() &&
580
			$lbFactory->hasOrMadeRecentMasterChanges( INF ) &&
581
			self::isWikiClusterURL( $output->getRedirect(), $context )
582
		) {
583
			// OutputPage::output() will be fast; $postCommitWork will not be useful for
584
			// masking the latency of syncing DB positions accross all datacenters synchronously.
585
			// Instead, make use of the RTT time of the client follow redirects.
586
			$flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
587
			// Client's next request should see 1+ positions with this DBMasterPos::asOf() time
588
			$safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
589
				$output->getRedirect(),
590
				microtime( true )
591
			);
592
			$output->redirect( $safeUrl );
593
		} else {
594
			// OutputPage::output() is fairly slow; run it in $postCommitWork to mask
595
			// the latency of syncing DB positions accross all datacenters synchronously
596
			$flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
597
		}
598
		// Record ChronologyProtector positions for DBs affected in this request at this point
599
		$lbFactory->shutdown( $flags, $postCommitWork );
600
		wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
601
602
		// Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
603
		// POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
604
		// ChronologyProtector works for cacheable URLs.
605
		if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentMasterChanges() ) {
606
			$expires = time() + $config->get( 'DataCenterUpdateStickTTL' );
607
			$options = [ 'prefix' => '' ];
608
			$request->response()->setCookie( 'UseDC', 'master', $expires, $options );
609
			$request->response()->setCookie( 'UseCDNCache', 'false', $expires, $options );
610
		}
611
612
		// Avoid letting a few seconds of replica DB lag cause a month of stale data. This logic is
613
		// also intimately related to the value of $wgCdnReboundPurgeDelay.
614
		if ( $lbFactory->laggedReplicaUsed() ) {
615
			$maxAge = $config->get( 'CdnMaxageLagged' );
616
			$output->lowerCdnMaxage( $maxAge );
617
			$request->response()->header( "X-Database-Lagged: true" );
618
			wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
619
		}
620
621
		// Avoid long-term cache pollution due to message cache rebuild timeouts (T133069)
622
		if ( MessageCache::singleton()->isDisabled() ) {
623
			$maxAge = $config->get( 'CdnMaxageSubstitute' );
624
			$output->lowerCdnMaxage( $maxAge );
625
			$request->response()->header( "X-Response-Substitute: true" );
626
		}
627
	}
628
629
	/**
630
	 * @param string $url
631
	 * @param IContextSource $context
632
	 * @return bool Whether $url is to something on this wiki farm
633
	 */
634
	private function isWikiClusterURL( $url, IContextSource $context ) {
635
		static $relevantKeys = [ 'host' => true, 'port' => true ];
636
637
		$infoCandidate = wfParseUrl( $url );
638
		if ( $infoCandidate === false ) {
639
			return false;
640
		}
641
642
		$infoCandidate = array_intersect_key( $infoCandidate, $relevantKeys );
643
		$clusterHosts = array_merge(
644
			// Local wiki host (the most common case)
645
			[ $context->getConfig()->get( 'CanonicalServer' ) ],
646
			// Any local/remote wiki virtual hosts for this wiki farm
647
			$context->getConfig()->get( 'LocalVirtualHosts' )
648
		);
649
650
		foreach ( $clusterHosts as $clusterHost ) {
651
			$infoHost = array_intersect_key( wfParseUrl( $clusterHost ), $relevantKeys );
652
			if ( $infoCandidate === $infoHost ) {
653
				return true;
654
			}
655
		}
656
657
		return false;
658
	}
659
660
	/**
661
	 * This function does work that can be done *after* the
662
	 * user gets the HTTP response so they don't block on it
663
	 *
664
	 * This manages deferred updates, job insertion,
665
	 * final commit, and the logging of profiling data
666
	 *
667
	 * @param string $mode Use 'fast' to always skip job running
668
	 * @since 1.26
669
	 */
670
	public function doPostOutputShutdown( $mode = 'normal' ) {
671
		$timing = $this->context->getTiming();
672
		$timing->mark( 'requestShutdown' );
673
674
		// Show visible profiling data if enabled (which cannot be post-send)
675
		Profiler::instance()->logDataPageOutputOnly();
676
677
		$callback = function () use ( $mode ) {
678
			try {
679
				$this->restInPeace( $mode );
680
			} catch ( Exception $e ) {
681
				MWExceptionHandler::handleException( $e );
682
			}
683
		};
684
685
		// Defer everything else...
686
		if ( function_exists( 'register_postsend_function' ) ) {
687
			// https://github.com/facebook/hhvm/issues/1230
688
			register_postsend_function( $callback );
689
		} else {
690
			if ( function_exists( 'fastcgi_finish_request' ) ) {
691
				fastcgi_finish_request();
692
			} else {
693
				// Either all DB and deferred updates should happen or none.
694
				// The latter should not be cancelled due to client disconnect.
695
				ignore_user_abort( true );
696
			}
697
698
			$callback();
699
		}
700
	}
701
702
	private function main() {
703
		global $wgTitle;
704
705
		$output = $this->context->getOutput();
706
		$request = $this->context->getRequest();
707
708
		// Send Ajax requests to the Ajax dispatcher.
709
		if ( $this->config->get( 'UseAjax' ) && $request->getVal( 'action' ) === 'ajax' ) {
710
			// Set a dummy title, because $wgTitle == null might break things
711
			$title = Title::makeTitle( NS_SPECIAL, 'Badtitle/performing an AJAX call in '
712
				. __METHOD__
713
			);
714
			$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...
715
			$wgTitle = $title;
716
717
			$dispatcher = new AjaxDispatcher( $this->config );
718
			$dispatcher->performAction( $this->context->getUser() );
719
720
			return;
721
		}
722
723
		// Get title from request parameters,
724
		// is set on the fly by parseTitle the first time.
725
		$title = $this->getTitle();
726
		$action = $this->getAction();
727
		$wgTitle = $title;
728
729
		// Set DB query expectations for this HTTP request
730
		$trxLimits = $this->config->get( 'TrxProfilerLimits' );
731
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
732
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
733
		if ( $request->hasSafeMethod() ) {
734
			$trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
735
		} else {
736
			$trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
737
		}
738
739
		// If the user has forceHTTPS set to true, or if the user
740
		// is in a group requiring HTTPS, or if they have the HTTPS
741
		// preference set, redirect them to HTTPS.
742
		// Note: Do this after $wgTitle is setup, otherwise the hooks run from
743
		// isLoggedIn() will do all sorts of weird stuff.
744
		if (
745
			$request->getProtocol() == 'http' &&
746
			// switch to HTTPS only when supported by the server
747
			preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) &&
748
			(
749
				$request->getSession()->shouldForceHTTPS() ||
750
				// Check the cookie manually, for paranoia
751
				$request->getCookie( 'forceHTTPS', '' ) ||
752
				// check for prefixed version that was used for a time in older MW versions
753
				$request->getCookie( 'forceHTTPS' ) ||
754
				// Avoid checking the user and groups unless it's enabled.
755
				(
756
					$this->context->getUser()->isLoggedIn()
757
					&& $this->context->getUser()->requiresHTTPS()
758
				)
759
			)
760
		) {
761
			$oldUrl = $request->getFullRequestURL();
762
			$redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
763
764
			// ATTENTION: This hook is likely to be removed soon due to overall design of the system.
765
			if ( Hooks::run( 'BeforeHttpsRedirect', [ $this->context, &$redirUrl ] ) ) {
766
767
				if ( $request->wasPosted() ) {
768
					// This is weird and we'd hope it almost never happens. This
769
					// means that a POST came in via HTTP and policy requires us
770
					// redirecting to HTTPS. It's likely such a request is going
771
					// to fail due to post data being lost, but let's try anyway
772
					// and just log the instance.
773
774
					// @todo FIXME: See if we could issue a 307 or 308 here, need
775
					// to see how clients (automated & browser) behave when we do
776
					wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
777
				}
778
				// Setup dummy Title, otherwise OutputPage::redirect will fail
779
				$title = Title::newFromText( 'REDIR', NS_MAIN );
780
				$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...
781
				// Since we only do this redir to change proto, always send a vary header
782
				$output->addVaryHeader( 'X-Forwarded-Proto' );
783
				$output->redirect( $redirUrl );
784
				$output->output();
785
786
				return;
787
			}
788
		}
789
790
		if ( $this->config->get( 'UseFileCache' ) && $title->getNamespace() >= 0 ) {
791
			if ( HTMLFileCache::useFileCache( $this->context ) ) {
792
				// Try low-level file cache hit
793
				$cache = new HTMLFileCache( $title, $action );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 725 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...
794
				if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
795
					// Check incoming headers to see if client has this cached
796
					$timestamp = $cache->cacheTimestamp();
797
					if ( !$output->checkLastModified( $timestamp ) ) {
0 ignored issues
show
Security Bug introduced by
It seems like $timestamp defined by $cache->cacheTimestamp() on line 796 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...
798
						$cache->loadFromFileCache( $this->context );
799
					}
800
					// Do any stats increment/watchlist stuff
801
					// Assume we're viewing the latest revision (this should always be the case with file cache)
802
					$this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
803
					// Tell OutputPage that output is taken care of
804
					$output->disable();
805
806
					return;
807
				}
808
			}
809
		}
810
811
		// Actually do the work of the request and build up any output
812
		$this->performRequest();
813
814
		// GUI-ify and stash the page output in MediaWiki::doPreOutputCommit() while
815
		// ChronologyProtector synchronizes DB positions or slaves accross all datacenters.
816
		$buffer = null;
817
		$outputWork = function () use ( $output, &$buffer ) {
818
			if ( $buffer === null ) {
819
				$buffer = $output->output( true );
820
			}
821
822
			return $buffer;
823
		};
824
825
		// Now commit any transactions, so that unreported errors after
826
		// output() don't roll back the whole DB transaction and so that
827
		// we avoid having both success and error text in the response
828
		$this->doPreOutputCommit( $outputWork );
829
830
		// Now send the actual output
831
		print $outputWork();
832
	}
833
834
	/**
835
	 * Ends this task peacefully
836
	 * @param string $mode Use 'fast' to always skip job running
837
	 */
838
	public function restInPeace( $mode = 'fast' ) {
839
		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
840
		// Assure deferred updates are not in the main transaction
841
		$lbFactory->commitMasterChanges( __METHOD__ );
842
843
		// Loosen DB query expectations since the HTTP client is unblocked
844
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
845
		$trxProfiler->resetExpectations();
846
		$trxProfiler->setExpectations(
847
			$this->config->get( 'TrxProfilerLimits' )['PostSend'],
848
			__METHOD__
849
		);
850
851
		// Do any deferred jobs
852
		DeferredUpdates::doUpdates( 'enqueue' );
853
854
		// Make sure any lazy jobs are pushed
855
		JobQueueGroup::pushLazyJobs();
856
857
		// Now that everything specific to this request is done,
858
		// try to occasionally run jobs (if enabled) from the queues
859
		if ( $mode === 'normal' ) {
860
			$this->triggerJobs();
861
		}
862
863
		// Log profiling data, e.g. in the database or UDP
864
		wfLogProfilingData();
865
866
		// Commit and close up!
867
		$lbFactory->commitMasterChanges( __METHOD__ );
868
		$lbFactory->shutdown( LBFactory::SHUTDOWN_NO_CHRONPROT );
869
870
		wfDebug( "Request ended normally\n" );
871
	}
872
873
	/**
874
	 * Potentially open a socket and sent an HTTP request back to the server
875
	 * to run a specified number of jobs. This registers a callback to cleanup
876
	 * the socket once it's done.
877
	 */
878
	public function triggerJobs() {
879
		$jobRunRate = $this->config->get( 'JobRunRate' );
880
		if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
881
			return; // recursion guard
882
		} elseif ( $jobRunRate <= 0 || wfReadOnly() ) {
883
			return;
884
		}
885
886
		if ( $jobRunRate < 1 ) {
887
			$max = mt_getrandmax();
888
			if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
889
				return; // the higher the job run rate, the less likely we return here
890
			}
891
			$n = 1;
892
		} else {
893
			$n = intval( $jobRunRate );
894
		}
895
896
		$runJobsLogger = LoggerFactory::getInstance( 'runJobs' );
897
898
		// Fall back to running the job(s) while the user waits if needed
899
		if ( !$this->config->get( 'RunJobsAsync' ) ) {
900
			$runner = new JobRunner( $runJobsLogger );
901
			$runner->run( [ 'maxJobs' => $n ] );
902
			return;
903
		}
904
905
		// Do not send request if there are probably no jobs
906
		try {
907
			$group = JobQueueGroup::singleton();
908
			if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
909
				return;
910
			}
911
		} catch ( JobQueueError $e ) {
912
			MWExceptionHandler::logException( $e );
913
			return; // do not make the site unavailable
914
		}
915
916
		$query = [ 'title' => 'Special:RunJobs',
917
			'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ];
918
		$query['signature'] = SpecialRunJobs::getQuerySignature(
919
			$query, $this->config->get( 'SecretKey' ) );
920
921
		$errno = $errstr = null;
922
		$info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
923
		$host = $info ? $info['host'] : null;
924
		$port = 80;
925
		if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) {
926
			$host = "tls://" . $host;
927
			$port = 443;
928
		}
929
		if ( isset( $info['port'] ) ) {
930
			$port = $info['port'];
931
		}
932
933
		MediaWiki\suppressWarnings();
934
		$sock = $host ? fsockopen(
935
			$host,
936
			$port,
937
			$errno,
938
			$errstr,
939
			// If it takes more than 100ms to connect to ourselves there is a problem...
940
			0.100
941
		) : false;
942
		MediaWiki\restoreWarnings();
943
944
		$invokedWithSuccess = true;
945
		if ( $sock ) {
946
			$special = SpecialPageFactory::getPage( 'RunJobs' );
947
			$url = $special->getPageTitle()->getCanonicalURL( $query );
948
			$req = (
949
				"POST $url HTTP/1.1\r\n" .
950
				"Host: {$info['host']}\r\n" .
951
				"Connection: Close\r\n" .
952
				"Content-Length: 0\r\n\r\n"
953
			);
954
955
			$runJobsLogger->info( "Running $n job(s) via '$url'" );
956
			// Send a cron API request to be performed in the background.
957
			// Give up if this takes too long to send (which should be rare).
958
			stream_set_timeout( $sock, 2 );
959
			$bytes = fwrite( $sock, $req );
960
			if ( $bytes !== strlen( $req ) ) {
961
				$invokedWithSuccess = false;
962
				$runJobsLogger->error( "Failed to start cron API (socket write error)" );
963
			} else {
964
				// Do not wait for the response (the script should handle client aborts).
965
				// Make sure that we don't close before that script reaches ignore_user_abort().
966
				$start = microtime( true );
967
				$status = fgets( $sock );
968
				$sec = microtime( true ) - $start;
969
				if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
970
					$invokedWithSuccess = false;
971
					$runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" );
972
				}
973
			}
974
			fclose( $sock );
975
		} else {
976
			$invokedWithSuccess = false;
977
			$runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" );
978
		}
979
980
		// Fall back to running the job(s) while the user waits if needed
981
		if ( !$invokedWithSuccess ) {
982
			$runJobsLogger->warning( "Jobs switched to blocking; Special:RunJobs disabled" );
983
984
			$runner = new JobRunner( $runJobsLogger );
985
			$runner->run( [ 'maxJobs'  => $n ] );
986
		}
987
	}
988
}
989