Completed
Branch master (5cbada)
by
unknown
28:59
created

WikiPage   F

Complexity

Total Complexity 411

Size/Duplication

Total Lines 3538
Duplicated Lines 1.61 %

Coupling/Cohesion

Components 1
Dependencies 47

Importance

Changes 2
Bugs 0 Features 2
Metric Value
dl 57
loc 3538
rs 0.5217
c 2
b 0
f 2
wmc 411
lcom 1
cbo 47

90 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
B factory() 0 22 5
A newFromID() 0 15 4
A newFromRow() 0 5 1
A convertSelectType() 0 13 4
A getActionOverrides() 0 3 1
A getContentHandler() 0 3 1
A getTitle() 0 3 1
A clear() 0 6 1
A clearCacheFields() 0 14 1
A clearPreparedEdit() 0 3 1
B selectFields() 0 27 3
A pageData() 0 11 1
A pageDataFromTitle() 0 5 1
A pageDataFromId() 0 3 1
C loadPageData() 0 28 8
B loadFromRow() 0 36 4
A getId() 0 6 2
A exists() 0 6 2
A hasViewableContent() 0 3 2
A isRedirect() 0 7 2
A getContentModel() 0 16 3
A checkTouched() 0 6 2
A getTouched() 0 6 2
A getLinksTimestamp() 0 6 2
A getLatest() 0 6 2
B getOldestRevision() 0 33 4
B loadLastEdit() 0 32 6
A setLastEdit() 0 4 1
A getRevision() 0 7 2
A getContent() 7 7 2
A getText() 0 9 2
A getTimestamp() 0 8 2
A setTimestamp() 0 3 1
A getUser() 8 8 2
A getCreator() 0 9 2
A getUserText() 8 8 2
A getComment() 8 8 2
A getMinorEdit() 0 8 2
C isCountable() 0 36 7
B getRedirectTarget() 0 30 6
A insertRedirect() 0 16 3
A insertRedirectEntry() 0 20 3
A followRedirect() 0 3 1
B getRedirectURL() 0 32 6
B getContributors() 0 46 3
B shouldCheckParserCache() 0 6 6
C getParserOutput() 0 25 7
A doViewUpdates() 0 14 3
B doPurge() 0 38 6
B insertOn() 0 30 4
C updateRevisionOn() 0 59 9
B updateRedirectOn() 0 24 5
B updateIfNewerOn() 0 26 3
A getUndoContent() 0 4 1
A supportsSections() 0 3 1
C replaceSectionContent() 0 25 7
C replaceSectionAtRev() 0 37 7
A checkFlags() 0 11 4
A doEdit() 0 7 1
D doEditContent() 0 83 13
C doModify() 0 154 11
C doCreate() 0 111 7
A makeParserOptions() 0 11 2
A prepareTextForEdit() 0 5 1
F prepareContentForEdit() 0 110 23
F doEditUpdates() 0 143 37
A doQuickEditContent() 0 23 2
F doUpdateRestrictions() 0 241 36
B insertProtectNullRevision() 0 40 5
A formatExpiry() 0 15 2
B protectDescription() 0 30 3
A protectDescriptionLog() 0 13 2
A flattenRestrictions() 0 14 3
A doDeleteArticle() 0 6 1
C doDeleteArticleReal() 10 152 11
A lockAndGetLatest() 0 15 1
B doDeleteUpdates() 0 27 3
B doRollback() 0 25 5
F commitRollback() 16 156 20
A onArticleCreate() 0 10 1
B onArticleDelete() 0 40 6
A onArticleEdit() 0 17 2
A getCategories() 0 16 2
B getHiddenCategories() 0 23 4
A getAutosummary() 0 12 3
A getAutoDeleteReason() 0 3 1
C updateCategoryCounts() 0 82 12
C triggerOpportunisticLinksUpdate() 0 44 8
A getDeletionUpdates() 0 16 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WikiPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WikiPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Base representation for a MediaWiki page.
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
/**
24
 * Class representing a MediaWiki article and history.
25
 *
26
 * Some fields are public only for backwards-compatibility. Use accessors.
27
 * In the past, this class was part of Article.php and everything was public.
28
 */
29
class WikiPage implements Page, IDBAccessObject {
30
	// Constants for $mDataLoadedFrom and related
31
32
	/**
33
	 * @var Title
34
	 */
35
	public $mTitle = null;
36
37
	/**@{{
38
	 * @protected
39
	 */
40
	public $mDataLoaded = false;         // !< Boolean
41
	public $mIsRedirect = false;         // !< Boolean
42
	public $mLatest = false;             // !< Integer (false means "not loaded")
43
	/**@}}*/
44
45
	/** @var stdClass Map of cache fields (text, parser output, ect) for a proposed/new edit */
46
	public $mPreparedEdit = false;
47
48
	/**
49
	 * @var int
50
	 */
51
	protected $mId = null;
52
53
	/**
54
	 * @var int One of the READ_* constants
55
	 */
56
	protected $mDataLoadedFrom = self::READ_NONE;
57
58
	/**
59
	 * @var Title
60
	 */
61
	protected $mRedirectTarget = null;
62
63
	/**
64
	 * @var Revision
65
	 */
66
	protected $mLastRevision = null;
67
68
	/**
69
	 * @var string Timestamp of the current revision or empty string if not loaded
70
	 */
71
	protected $mTimestamp = '';
72
73
	/**
74
	 * @var string
75
	 */
76
	protected $mTouched = '19700101000000';
77
78
	/**
79
	 * @var string
80
	 */
81
	protected $mLinksUpdated = '19700101000000';
82
83
	/**
84
	 * Constructor and clear the article
85
	 * @param Title $title Reference to a Title object.
86
	 */
87
	public function __construct( Title $title ) {
88
		$this->mTitle = $title;
89
	}
90
91
	/**
92
	 * Create a WikiPage object of the appropriate class for the given title.
93
	 *
94
	 * @param Title $title
95
	 *
96
	 * @throws MWException
97
	 * @return WikiPage|WikiCategoryPage|WikiFilePage
98
	 */
99
	public static function factory( Title $title ) {
100
		$ns = $title->getNamespace();
101
102
		if ( $ns == NS_MEDIA ) {
103
			throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
104
		} elseif ( $ns < 0 ) {
105
			throw new MWException( "Invalid or virtual namespace $ns given." );
106
		}
107
108
		switch ( $ns ) {
109
			case NS_FILE:
110
				$page = new WikiFilePage( $title );
111
				break;
112
			case NS_CATEGORY:
113
				$page = new WikiCategoryPage( $title );
114
				break;
115
			default:
116
				$page = new WikiPage( $title );
117
		}
118
119
		return $page;
120
	}
121
122
	/**
123
	 * Constructor from a page id
124
	 *
125
	 * @param int $id Article ID to load
126
	 * @param string|int $from One of the following values:
127
	 *        - "fromdb" or WikiPage::READ_NORMAL to select from a slave database
128
	 *        - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
129
	 *
130
	 * @return WikiPage|null
131
	 */
132
	public static function newFromID( $id, $from = 'fromdb' ) {
133
		// page id's are never 0 or negative, see bug 61166
134
		if ( $id < 1 ) {
135
			return null;
136
		}
137
138
		$from = self::convertSelectType( $from );
139
		$db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
140
		$row = $db->selectRow(
141
			'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
142
		if ( !$row ) {
143
			return null;
144
		}
145
		return self::newFromRow( $row, $from );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $db->selectRow('page', s...d' => $id), __METHOD__) on line 140 can also be of type boolean; however, WikiPage::newFromRow() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $from defined by self::convertSelectType($from) on line 138 can also be of type object; however, WikiPage::newFromRow() does only seem to accept string|integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
146
	}
147
148
	/**
149
	 * Constructor from a database row
150
	 *
151
	 * @since 1.20
152
	 * @param object $row Database row containing at least fields returned by selectFields().
153
	 * @param string|int $from Source of $data:
154
	 *        - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
155
	 *        - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
156
	 *        - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
157
	 * @return WikiPage
158
	 */
159
	public static function newFromRow( $row, $from = 'fromdb' ) {
160
		$page = self::factory( Title::newFromRow( $row ) );
161
		$page->loadFromRow( $row, $from );
162
		return $page;
163
	}
164
165
	/**
166
	 * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
167
	 *
168
	 * @param object|string|int $type
169
	 * @return mixed
170
	 */
171
	private static function convertSelectType( $type ) {
172
		switch ( $type ) {
173
		case 'fromdb':
174
			return self::READ_NORMAL;
175
		case 'fromdbmaster':
176
			return self::READ_LATEST;
177
		case 'forupdate':
178
			return self::READ_LOCKING;
179
		default:
180
			// It may already be an integer or whatever else
181
			return $type;
182
		}
183
	}
184
185
	/**
186
	 * @todo Move this UI stuff somewhere else
187
	 *
188
	 * @see ContentHandler::getActionOverrides
189
	 */
190
	public function getActionOverrides() {
191
		return $this->getContentHandler()->getActionOverrides();
192
	}
193
194
	/**
195
	 * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
196
	 *
197
	 * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
198
	 *
199
	 * @return ContentHandler
200
	 *
201
	 * @since 1.21
202
	 */
203
	public function getContentHandler() {
204
		return ContentHandler::getForModelID( $this->getContentModel() );
205
	}
206
207
	/**
208
	 * Get the title object of the article
209
	 * @return Title Title object of this page
210
	 */
211
	public function getTitle() {
212
		return $this->mTitle;
213
	}
214
215
	/**
216
	 * Clear the object
217
	 * @return void
218
	 */
219
	public function clear() {
220
		$this->mDataLoaded = false;
221
		$this->mDataLoadedFrom = self::READ_NONE;
222
223
		$this->clearCacheFields();
224
	}
225
226
	/**
227
	 * Clear the object cache fields
228
	 * @return void
229
	 */
230
	protected function clearCacheFields() {
231
		$this->mId = null;
232
		$this->mRedirectTarget = null; // Title object if set
233
		$this->mLastRevision = null; // Latest revision
234
		$this->mTouched = '19700101000000';
235
		$this->mLinksUpdated = '19700101000000';
236
		$this->mTimestamp = '';
237
		$this->mIsRedirect = false;
238
		$this->mLatest = false;
239
		// Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
240
		// the requested rev ID and content against the cached one for equality. For most
241
		// content types, the output should not change during the lifetime of this cache.
242
		// Clearing it can cause extra parses on edit for no reason.
243
	}
244
245
	/**
246
	 * Clear the mPreparedEdit cache field, as may be needed by mutable content types
247
	 * @return void
248
	 * @since 1.23
249
	 */
250
	public function clearPreparedEdit() {
251
		$this->mPreparedEdit = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<stdClass> of property $mPreparedEdit.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
252
	}
253
254
	/**
255
	 * Return the list of revision fields that should be selected to create
256
	 * a new page.
257
	 *
258
	 * @return array
259
	 */
260
	public static function selectFields() {
261
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
262
263
		$fields = [
264
			'page_id',
265
			'page_namespace',
266
			'page_title',
267
			'page_restrictions',
268
			'page_is_redirect',
269
			'page_is_new',
270
			'page_random',
271
			'page_touched',
272
			'page_links_updated',
273
			'page_latest',
274
			'page_len',
275
		];
276
277
		if ( $wgContentHandlerUseDB ) {
278
			$fields[] = 'page_content_model';
279
		}
280
281
		if ( $wgPageLanguageUseDB ) {
282
			$fields[] = 'page_lang';
283
		}
284
285
		return $fields;
286
	}
287
288
	/**
289
	 * Fetch a page record with the given conditions
290
	 * @param IDatabase $dbr
291
	 * @param array $conditions
292
	 * @param array $options
293
	 * @return object|bool Database result resource, or false on failure
294
	 */
295
	protected function pageData( $dbr, $conditions, $options = [] ) {
296
		$fields = self::selectFields();
297
298
		Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
299
300
		$row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
301
302
		Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
303
304
		return $row;
305
	}
306
307
	/**
308
	 * Fetch a page record matching the Title object's namespace and title
309
	 * using a sanitized title string
310
	 *
311
	 * @param IDatabase $dbr
312
	 * @param Title $title
313
	 * @param array $options
314
	 * @return object|bool Database result resource, or false on failure
315
	 */
316
	public function pageDataFromTitle( $dbr, $title, $options = [] ) {
317
		return $this->pageData( $dbr, [
318
			'page_namespace' => $title->getNamespace(),
319
			'page_title' => $title->getDBkey() ], $options );
320
	}
321
322
	/**
323
	 * Fetch a page record matching the requested ID
324
	 *
325
	 * @param IDatabase $dbr
326
	 * @param int $id
327
	 * @param array $options
328
	 * @return object|bool Database result resource, or false on failure
329
	 */
330
	public function pageDataFromId( $dbr, $id, $options = [] ) {
331
		return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
332
	}
333
334
	/**
335
	 * Load the object from a given source by title
336
	 *
337
	 * @param object|string|int $from One of the following:
338
	 *   - A DB query result object.
339
	 *   - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB.
340
	 *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
341
	 *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
342
	 *     using SELECT FOR UPDATE.
343
	 *
344
	 * @return void
345
	 */
346
	public function loadPageData( $from = 'fromdb' ) {
347
		$from = self::convertSelectType( $from );
348
		if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
349
			// We already have the data from the correct location, no need to load it twice.
350
			return;
351
		}
352
353
		if ( is_int( $from ) ) {
354
			list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
355
			$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
356
357
			if ( !$data
358
				&& $index == DB_SLAVE
359
				&& wfGetLB()->getServerCount() > 1
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or 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...
360
				&& wfGetLB()->hasOrMadeRecentMasterChanges()
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or 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...
361
			) {
362
				$from = self::READ_LATEST;
363
				list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
364
				$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
365
			}
366
		} else {
367
			// No idea from where the caller got this data, assume slave database.
368
			$data = $from;
369
			$from = self::READ_NORMAL;
370
		}
371
372
		$this->loadFromRow( $data, $from );
0 ignored issues
show
Bug introduced by
It seems like $data defined by $from on line 368 can also be of type string; however, WikiPage::loadFromRow() does only seem to accept object|boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
373
	}
374
375
	/**
376
	 * Load the object from a database row
377
	 *
378
	 * @since 1.20
379
	 * @param object|bool $data DB row containing fields returned by selectFields() or false
380
	 * @param string|int $from One of the following:
381
	 *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
382
	 *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
383
	 *        - "forupdate"  or WikiPage::READ_LOCKING if the data comes from
384
	 *          the master DB using SELECT FOR UPDATE
385
	 */
386
	public function loadFromRow( $data, $from ) {
387
		$lc = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
388
		$lc->clearLink( $this->mTitle );
389
390
		if ( $data ) {
391
			$lc->addGoodLinkObjFromRow( $this->mTitle, $data );
392
393
			$this->mTitle->loadFromRow( $data );
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 386 can also be of type object; however, Title::loadFromRow() does only seem to accept object<stdClass>|boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
394
395
			// Old-fashioned restrictions
396
			$this->mTitle->loadRestrictions( $data->page_restrictions );
397
398
			$this->mId = intval( $data->page_id );
399
			$this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestamp(TS_MW, $data->page_touched) can also be of type false. However, the property $mTouched is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
400
			$this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestampOrNull(TS_MW,...ta->page_links_updated) can also be of type false. However, the property $mLinksUpdated is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
401
			$this->mIsRedirect = intval( $data->page_is_redirect );
0 ignored issues
show
Documentation Bug introduced by
The property $mIsRedirect was declared of type boolean, but intval($data->page_is_redirect) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
402
			$this->mLatest = intval( $data->page_latest );
0 ignored issues
show
Documentation Bug introduced by
The property $mLatest was declared of type boolean, but intval($data->page_latest) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
403
			// Bug 37225: $latest may no longer match the cached latest Revision object.
404
			// Double-check the ID of any cached latest Revision object for consistency.
405
			if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
406
				$this->mLastRevision = null;
407
				$this->mTimestamp = '';
408
			}
409
		} else {
410
			$lc->addBadLinkObj( $this->mTitle );
411
412
			$this->mTitle->loadFromRow( false );
413
414
			$this->clearCacheFields();
415
416
			$this->mId = 0;
417
		}
418
419
		$this->mDataLoaded = true;
420
		$this->mDataLoadedFrom = self::convertSelectType( $from );
0 ignored issues
show
Documentation Bug introduced by
It seems like self::convertSelectType($from) can also be of type object or string. However, the property $mDataLoadedFrom is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
421
	}
422
423
	/**
424
	 * @return int Page ID
425
	 */
426
	public function getId() {
427
		if ( !$this->mDataLoaded ) {
428
			$this->loadPageData();
429
		}
430
		return $this->mId;
431
	}
432
433
	/**
434
	 * @return bool Whether or not the page exists in the database
435
	 */
436
	public function exists() {
437
		if ( !$this->mDataLoaded ) {
438
			$this->loadPageData();
439
		}
440
		return $this->mId > 0;
441
	}
442
443
	/**
444
	 * Check if this page is something we're going to be showing
445
	 * some sort of sensible content for. If we return false, page
446
	 * views (plain action=view) will return an HTTP 404 response,
447
	 * so spiders and robots can know they're following a bad link.
448
	 *
449
	 * @return bool
450
	 */
451
	public function hasViewableContent() {
452
		return $this->exists() || $this->mTitle->isAlwaysKnown();
453
	}
454
455
	/**
456
	 * Tests if the article content represents a redirect
457
	 *
458
	 * @return bool
459
	 */
460
	public function isRedirect() {
461
		if ( !$this->mDataLoaded ) {
462
			$this->loadPageData();
463
		}
464
465
		return (bool)$this->mIsRedirect;
466
	}
467
468
	/**
469
	 * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
470
	 *
471
	 * Will use the revisions actual content model if the page exists,
472
	 * and the page's default if the page doesn't exist yet.
473
	 *
474
	 * @return string
475
	 *
476
	 * @since 1.21
477
	 */
478
	public function getContentModel() {
479
		if ( $this->exists() ) {
480
			// look at the revision's actual content model
481
			$rev = $this->getRevision();
482
483
			if ( $rev !== null ) {
484
				return $rev->getContentModel();
485
			} else {
486
				$title = $this->mTitle->getPrefixedDBkey();
487
				wfWarn( "Page $title exists but has no (visible) revisions!" );
488
			}
489
		}
490
491
		// use the default model for this page
492
		return $this->mTitle->getContentModel();
493
	}
494
495
	/**
496
	 * Loads page_touched and returns a value indicating if it should be used
497
	 * @return bool True if not a redirect
498
	 */
499
	public function checkTouched() {
500
		if ( !$this->mDataLoaded ) {
501
			$this->loadPageData();
502
		}
503
		return !$this->mIsRedirect;
504
	}
505
506
	/**
507
	 * Get the page_touched field
508
	 * @return string Containing GMT timestamp
509
	 */
510
	public function getTouched() {
511
		if ( !$this->mDataLoaded ) {
512
			$this->loadPageData();
513
		}
514
		return $this->mTouched;
515
	}
516
517
	/**
518
	 * Get the page_links_updated field
519
	 * @return string|null Containing GMT timestamp
520
	 */
521
	public function getLinksTimestamp() {
522
		if ( !$this->mDataLoaded ) {
523
			$this->loadPageData();
524
		}
525
		return $this->mLinksUpdated;
526
	}
527
528
	/**
529
	 * Get the page_latest field
530
	 * @return int The rev_id of current revision
531
	 */
532
	public function getLatest() {
533
		if ( !$this->mDataLoaded ) {
534
			$this->loadPageData();
535
		}
536
		return (int)$this->mLatest;
537
	}
538
539
	/**
540
	 * Get the Revision object of the oldest revision
541
	 * @return Revision|null
542
	 */
543
	public function getOldestRevision() {
544
545
		// Try using the slave database first, then try the master
546
		$continue = 2;
547
		$db = wfGetDB( DB_SLAVE );
548
		$revSelectFields = Revision::selectFields();
549
550
		$row = null;
551
		while ( $continue ) {
552
			$row = $db->selectRow(
553
				[ 'page', 'revision' ],
554
				$revSelectFields,
555
				[
556
					'page_namespace' => $this->mTitle->getNamespace(),
557
					'page_title' => $this->mTitle->getDBkey(),
558
					'rev_page = page_id'
559
				],
560
				__METHOD__,
561
				[
562
					'ORDER BY' => 'rev_timestamp ASC'
563
				]
564
			);
565
566
			if ( $row ) {
567
				$continue = 0;
568
			} else {
569
				$db = wfGetDB( DB_MASTER );
570
				$continue--;
571
			}
572
		}
573
574
		return $row ? Revision::newFromRow( $row ) : null;
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type boolean; however, Revision::newFromRow() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
575
	}
576
577
	/**
578
	 * Loads everything except the text
579
	 * This isn't necessary for all uses, so it's only done if needed.
580
	 */
581
	protected function loadLastEdit() {
582
		if ( $this->mLastRevision !== null ) {
583
			return; // already loaded
584
		}
585
586
		$latest = $this->getLatest();
587
		if ( !$latest ) {
588
			return; // page doesn't exist or is missing page_latest info
589
		}
590
591
		if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
592
			// Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
593
			// includes the latest changes committed. This is true even within REPEATABLE-READ
594
			// transactions, where S1 normally only sees changes committed before the first S1
595
			// SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
596
			// may not find it since a page row UPDATE and revision row INSERT by S2 may have
597
			// happened after the first S1 SELECT.
598
			// http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
599
			$flags = Revision::READ_LOCKING;
600
		} elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
601
			// Bug T93976: if page_latest was loaded from the master, fetch the
602
			// revision from there as well, as it may not exist yet on a slave DB.
603
			// Also, this keeps the queries in the same REPEATABLE-READ snapshot.
604
			$flags = Revision::READ_LATEST;
605
		} else {
606
			$flags = 0;
607
		}
608
		$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
609
		if ( $revision ) { // sanity
610
			$this->setLastEdit( $revision );
611
		}
612
	}
613
614
	/**
615
	 * Set the latest revision
616
	 * @param Revision $revision
617
	 */
618
	protected function setLastEdit( Revision $revision ) {
619
		$this->mLastRevision = $revision;
620
		$this->mTimestamp = $revision->getTimestamp();
0 ignored issues
show
Documentation Bug introduced by
It seems like $revision->getTimestamp() can also be of type false. However, the property $mTimestamp is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
621
	}
622
623
	/**
624
	 * Get the latest revision
625
	 * @return Revision|null
626
	 */
627
	public function getRevision() {
628
		$this->loadLastEdit();
629
		if ( $this->mLastRevision ) {
630
			return $this->mLastRevision;
631
		}
632
		return null;
633
	}
634
635
	/**
636
	 * Get the content of the current revision. No side-effects...
637
	 *
638
	 * @param int $audience One of:
639
	 *   Revision::FOR_PUBLIC       to be displayed to all users
640
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
641
	 *   Revision::RAW              get the text regardless of permissions
642
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
643
	 *   to the $audience parameter
644
	 * @return Content|null The content of the current revision
645
	 *
646
	 * @since 1.21
647
	 */
648 View Code Duplication
	public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
649
		$this->loadLastEdit();
650
		if ( $this->mLastRevision ) {
651
			return $this->mLastRevision->getContent( $audience, $user );
652
		}
653
		return null;
654
	}
655
656
	/**
657
	 * Get the text of the current revision. No side-effects...
658
	 *
659
	 * @param int $audience One of:
660
	 *   Revision::FOR_PUBLIC       to be displayed to all users
661
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
662
	 *   Revision::RAW              get the text regardless of permissions
663
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
664
	 *   to the $audience parameter
665
	 * @return string|bool The text of the current revision
666
	 * @deprecated since 1.21, getContent() should be used instead.
667
	 */
668
	public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
669
		ContentHandler::deprecated( __METHOD__, '1.21' );
670
671
		$this->loadLastEdit();
672
		if ( $this->mLastRevision ) {
673
			return $this->mLastRevision->getText( $audience, $user );
0 ignored issues
show
Deprecated Code introduced by
The method Revision::getText() has been deprecated with message: since 1.21, use getContent() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
674
		}
675
		return false;
676
	}
677
678
	/**
679
	 * @return string MW timestamp of last article revision
680
	 */
681
	public function getTimestamp() {
682
		// Check if the field has been filled by WikiPage::setTimestamp()
683
		if ( !$this->mTimestamp ) {
684
			$this->loadLastEdit();
685
		}
686
687
		return wfTimestamp( TS_MW, $this->mTimestamp );
688
	}
689
690
	/**
691
	 * Set the page timestamp (use only to avoid DB queries)
692
	 * @param string $ts MW timestamp of last article revision
693
	 * @return void
694
	 */
695
	public function setTimestamp( $ts ) {
696
		$this->mTimestamp = wfTimestamp( TS_MW, $ts );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestamp(TS_MW, $ts) can also be of type false. However, the property $mTimestamp is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
697
	}
698
699
	/**
700
	 * @param int $audience One of:
701
	 *   Revision::FOR_PUBLIC       to be displayed to all users
702
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
703
	 *   Revision::RAW              get the text regardless of permissions
704
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
705
	 *   to the $audience parameter
706
	 * @return int User ID for the user that made the last article revision
707
	 */
708 View Code Duplication
	public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
709
		$this->loadLastEdit();
710
		if ( $this->mLastRevision ) {
711
			return $this->mLastRevision->getUser( $audience, $user );
712
		} else {
713
			return -1;
714
		}
715
	}
716
717
	/**
718
	 * Get the User object of the user who created the page
719
	 * @param int $audience One of:
720
	 *   Revision::FOR_PUBLIC       to be displayed to all users
721
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
722
	 *   Revision::RAW              get the text regardless of permissions
723
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
724
	 *   to the $audience parameter
725
	 * @return User|null
726
	 */
727
	public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
728
		$revision = $this->getOldestRevision();
729
		if ( $revision ) {
730
			$userName = $revision->getUserText( $audience, $user );
731
			return User::newFromName( $userName, false );
732
		} else {
733
			return null;
734
		}
735
	}
736
737
	/**
738
	 * @param int $audience One of:
739
	 *   Revision::FOR_PUBLIC       to be displayed to all users
740
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
741
	 *   Revision::RAW              get the text regardless of permissions
742
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
743
	 *   to the $audience parameter
744
	 * @return string Username of the user that made the last article revision
745
	 */
746 View Code Duplication
	public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
747
		$this->loadLastEdit();
748
		if ( $this->mLastRevision ) {
749
			return $this->mLastRevision->getUserText( $audience, $user );
750
		} else {
751
			return '';
752
		}
753
	}
754
755
	/**
756
	 * @param int $audience One of:
757
	 *   Revision::FOR_PUBLIC       to be displayed to all users
758
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
759
	 *   Revision::RAW              get the text regardless of permissions
760
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
761
	 *   to the $audience parameter
762
	 * @return string Comment stored for the last article revision
763
	 */
764 View Code Duplication
	public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
765
		$this->loadLastEdit();
766
		if ( $this->mLastRevision ) {
767
			return $this->mLastRevision->getComment( $audience, $user );
768
		} else {
769
			return '';
770
		}
771
	}
772
773
	/**
774
	 * Returns true if last revision was marked as "minor edit"
775
	 *
776
	 * @return bool Minor edit indicator for the last article revision.
777
	 */
778
	public function getMinorEdit() {
779
		$this->loadLastEdit();
780
		if ( $this->mLastRevision ) {
781
			return $this->mLastRevision->isMinor();
782
		} else {
783
			return false;
784
		}
785
	}
786
787
	/**
788
	 * Determine whether a page would be suitable for being counted as an
789
	 * article in the site_stats table based on the title & its content
790
	 *
791
	 * @param object|bool $editInfo (false): object returned by prepareTextForEdit(),
792
	 *   if false, the current database state will be used
793
	 * @return bool
794
	 */
795
	public function isCountable( $editInfo = false ) {
796
		global $wgArticleCountMethod;
797
798
		if ( !$this->mTitle->isContentPage() ) {
799
			return false;
800
		}
801
802
		if ( $editInfo ) {
803
			$content = $editInfo->pstContent;
804
		} else {
805
			$content = $this->getContent();
806
		}
807
808
		if ( !$content || $content->isRedirect() ) {
809
			return false;
810
		}
811
812
		$hasLinks = null;
813
814
		if ( $wgArticleCountMethod === 'link' ) {
815
			// nasty special case to avoid re-parsing to detect links
816
817
			if ( $editInfo ) {
818
				// ParserOutput::getLinks() is a 2D array of page links, so
819
				// to be really correct we would need to recurse in the array
820
				// but the main array should only have items in it if there are
821
				// links.
822
				$hasLinks = (bool)count( $editInfo->output->getLinks() );
823
			} else {
824
				$hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
825
					[ 'pl_from' => $this->getId() ], __METHOD__ );
826
			}
827
		}
828
829
		return $content->isCountable( $hasLinks );
830
	}
831
832
	/**
833
	 * If this page is a redirect, get its target
834
	 *
835
	 * The target will be fetched from the redirect table if possible.
836
	 * If this page doesn't have an entry there, call insertRedirect()
837
	 * @return Title|null Title object, or null if this page is not a redirect
838
	 */
839
	public function getRedirectTarget() {
840
		if ( !$this->mTitle->isRedirect() ) {
841
			return null;
842
		}
843
844
		if ( $this->mRedirectTarget !== null ) {
845
			return $this->mRedirectTarget;
846
		}
847
848
		// Query the redirect table
849
		$dbr = wfGetDB( DB_SLAVE );
850
		$row = $dbr->selectRow( 'redirect',
851
			[ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
852
			[ 'rd_from' => $this->getId() ],
853
			__METHOD__
854
		);
855
856
		// rd_fragment and rd_interwiki were added later, populate them if empty
857
		if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
858
			$this->mRedirectTarget = Title::makeTitle(
859
				$row->rd_namespace, $row->rd_title,
860
				$row->rd_fragment, $row->rd_interwiki
861
			);
862
			return $this->mRedirectTarget;
863
		}
864
865
		// This page doesn't have an entry in the redirect table
866
		$this->mRedirectTarget = $this->insertRedirect();
867
		return $this->mRedirectTarget;
868
	}
869
870
	/**
871
	 * Insert an entry for this page into the redirect table if the content is a redirect
872
	 *
873
	 * The database update will be deferred via DeferredUpdates
874
	 *
875
	 * Don't call this function directly unless you know what you're doing.
876
	 * @return Title|null Title object or null if not a redirect
877
	 */
878
	public function insertRedirect() {
879
		$content = $this->getContent();
880
		$retval = $content ? $content->getUltimateRedirectTarget() : null;
881
		if ( !$retval ) {
882
			return null;
883
		}
884
885
		// Update the DB post-send if the page has not cached since now
886
		$that = $this;
887
		$latest = $this->getLatest();
888
		DeferredUpdates::addCallableUpdate( function() use ( $that, $retval, $latest ) {
889
			$that->insertRedirectEntry( $retval, $latest );
890
		} );
891
892
		return $retval;
893
	}
894
895
	/**
896
	 * Insert or update the redirect table entry for this page to indicate it redirects to $rt
897
	 * @param Title $rt Redirect target
898
	 * @param int|null $oldLatest Prior page_latest for check and set
899
	 */
900
	public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
901
		$dbw = wfGetDB( DB_MASTER );
902
		$dbw->startAtomic( __METHOD__ );
903
904
		if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oldLatest of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
905
			$dbw->replace( 'redirect',
906
				[ 'rd_from' ],
907
				[
908
					'rd_from' => $this->getId(),
909
					'rd_namespace' => $rt->getNamespace(),
910
					'rd_title' => $rt->getDBkey(),
911
					'rd_fragment' => $rt->getFragment(),
912
					'rd_interwiki' => $rt->getInterwiki(),
913
				],
914
				__METHOD__
915
			);
916
		}
917
918
		$dbw->endAtomic( __METHOD__ );
919
	}
920
921
	/**
922
	 * Get the Title object or URL this page redirects to
923
	 *
924
	 * @return bool|Title|string False, Title of in-wiki target, or string with URL
925
	 */
926
	public function followRedirect() {
927
		return $this->getRedirectURL( $this->getRedirectTarget() );
0 ignored issues
show
Bug introduced by
It seems like $this->getRedirectTarget() can be null; however, getRedirectURL() 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...
928
	}
929
930
	/**
931
	 * Get the Title object or URL to use for a redirect. We use Title
932
	 * objects for same-wiki, non-special redirects and URLs for everything
933
	 * else.
934
	 * @param Title $rt Redirect target
935
	 * @return bool|Title|string False, Title object of local target, or string with URL
936
	 */
937
	public function getRedirectURL( $rt ) {
938
		if ( !$rt ) {
939
			return false;
940
		}
941
942
		if ( $rt->isExternal() ) {
943
			if ( $rt->isLocal() ) {
944
				// Offsite wikis need an HTTP redirect.
945
				// This can be hard to reverse and may produce loops,
946
				// so they may be disabled in the site configuration.
947
				$source = $this->mTitle->getFullURL( 'redirect=no' );
948
				return $rt->getFullURL( [ 'rdfrom' => $source ] );
949
			} else {
950
				// External pages without "local" bit set are not valid
951
				// redirect targets
952
				return false;
953
			}
954
		}
955
956
		if ( $rt->isSpecialPage() ) {
957
			// Gotta handle redirects to special pages differently:
958
			// Fill the HTTP response "Location" header and ignore the rest of the page we're on.
959
			// Some pages are not valid targets.
960
			if ( $rt->isValidRedirectTarget() ) {
961
				return $rt->getFullURL();
962
			} else {
963
				return false;
964
			}
965
		}
966
967
		return $rt;
968
	}
969
970
	/**
971
	 * Get a list of users who have edited this article, not including the user who made
972
	 * the most recent revision, which you can get from $article->getUser() if you want it
973
	 * @return UserArrayFromResult
974
	 */
975
	public function getContributors() {
976
		// @todo FIXME: This is expensive; cache this info somewhere.
977
978
		$dbr = wfGetDB( DB_SLAVE );
979
980
		if ( $dbr->implicitGroupby() ) {
981
			$realNameField = 'user_real_name';
982
		} else {
983
			$realNameField = 'MIN(user_real_name) AS user_real_name';
984
		}
985
986
		$tables = [ 'revision', 'user' ];
987
988
		$fields = [
989
			'user_id' => 'rev_user',
990
			'user_name' => 'rev_user_text',
991
			$realNameField,
992
			'timestamp' => 'MAX(rev_timestamp)',
993
		];
994
995
		$conds = [ 'rev_page' => $this->getId() ];
996
997
		// The user who made the top revision gets credited as "this page was last edited by
998
		// John, based on contributions by Tom, Dick and Harry", so don't include them twice.
999
		$user = $this->getUser();
1000
		if ( $user ) {
1001
			$conds[] = "rev_user != $user";
1002
		} else {
1003
			$conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1004
		}
1005
1006
		// Username hidden?
1007
		$conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1008
1009
		$jconds = [
1010
			'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1011
		];
1012
1013
		$options = [
1014
			'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1015
			'ORDER BY' => 'timestamp DESC',
1016
		];
1017
1018
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1019
		return new UserArrayFromResult( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select($tables, $f...D__, $options, $jconds) on line 1018 can also be of type boolean; however, UserArrayFromResult::__construct() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1020
	}
1021
1022
	/**
1023
	 * Should the parser cache be used?
1024
	 *
1025
	 * @param ParserOptions $parserOptions ParserOptions to check
1026
	 * @param int $oldId
1027
	 * @return bool
1028
	 */
1029
	public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1030
		return $parserOptions->getStubThreshold() == 0
1031
			&& $this->exists()
1032
			&& ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1033
			&& $this->getContentHandler()->isParserCacheSupported();
1034
	}
1035
1036
	/**
1037
	 * Get a ParserOutput for the given ParserOptions and revision ID.
1038
	 *
1039
	 * The parser cache will be used if possible. Cache misses that result
1040
	 * in parser runs are debounced with PoolCounter.
1041
	 *
1042
	 * @since 1.19
1043
	 * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
1044
	 * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
1045
	 *   get the current revision (default value)
1046
	 *
1047
	 * @return ParserOutput|bool ParserOutput or false if the revision was not found
1048
	 */
1049
	public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
1050
1051
		$useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
1052
		wfDebug( __METHOD__ .
1053
			': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1054
		if ( $parserOptions->getStubThreshold() ) {
1055
			wfIncrStats( 'pcache.miss.stub' );
1056
		}
1057
1058
		if ( $useParserCache ) {
1059
			$parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1060
			if ( $parserOutput !== false ) {
1061
				return $parserOutput;
1062
			}
1063
		}
1064
1065
		if ( $oldid === null || $oldid === 0 ) {
1066
			$oldid = $this->getLatest();
1067
		}
1068
1069
		$pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1070
		$pool->execute();
1071
1072
		return $pool->getParserOutput();
1073
	}
1074
1075
	/**
1076
	 * Do standard deferred updates after page view (existing or missing page)
1077
	 * @param User $user The relevant user
1078
	 * @param int $oldid Revision id being viewed; if not given or 0, latest revision is assumed
1079
	 */
1080
	public function doViewUpdates( User $user, $oldid = 0 ) {
1081
		if ( wfReadOnly() ) {
1082
			return;
1083
		}
1084
1085
		Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1086
		// Update newtalk / watchlist notification status
1087
		try {
1088
			$user->clearNotification( $this->mTitle, $oldid );
1089
		} catch ( DBError $e ) {
1090
			// Avoid outage if the master is not reachable
1091
			MWExceptionHandler::logException( $e );
1092
		}
1093
	}
1094
1095
	/**
1096
	 * Perform the actions of a page purging
1097
	 * @return bool
1098
	 */
1099
	public function doPurge() {
1100
		if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
1101
			return false;
1102
		}
1103
1104
		$title = $this->mTitle;
1105
		wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
1106
			// Invalidate the cache in auto-commit mode
1107
			$title->invalidateCache();
1108
		} );
1109
1110
		// Send purge after above page_touched update was committed
1111
		DeferredUpdates::addUpdate(
1112
			new CdnCacheUpdate( $title->getCdnUrls() ),
1113
			DeferredUpdates::PRESEND
1114
		);
1115
1116
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1117
			// @todo move this logic to MessageCache
1118
			if ( $this->exists() ) {
1119
				// NOTE: use transclusion text for messages.
1120
				//       This is consistent with  MessageCache::getMsgFromNamespace()
1121
1122
				$content = $this->getContent();
1123
				$text = $content === null ? null : $content->getWikitextForTransclusion();
1124
1125
				if ( $text === null ) {
1126
					$text = false;
1127
				}
1128
			} else {
1129
				$text = false;
1130
			}
1131
1132
			MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1133
		}
1134
1135
		return true;
1136
	}
1137
1138
	/**
1139
	 * Insert a new empty page record for this article.
1140
	 * This *must* be followed up by creating a revision
1141
	 * and running $this->updateRevisionOn( ... );
1142
	 * or else the record will be left in a funky state.
1143
	 * Best if all done inside a transaction.
1144
	 *
1145
	 * @param IDatabase $dbw
1146
	 * @param int|null $pageId Custom page ID that will be used for the insert statement
1147
	 *
1148
	 * @return bool|int The newly created page_id key; false if the title already existed
1149
	 */
1150
	public function insertOn( $dbw, $pageId = null ) {
1151
		$pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
1152
		$dbw->insert(
1153
			'page',
1154
			[
1155
				'page_id'           => $pageIdForInsert,
1156
				'page_namespace'    => $this->mTitle->getNamespace(),
1157
				'page_title'        => $this->mTitle->getDBkey(),
1158
				'page_restrictions' => '',
1159
				'page_is_redirect'  => 0, // Will set this shortly...
1160
				'page_is_new'       => 1,
1161
				'page_random'       => wfRandom(),
1162
				'page_touched'      => $dbw->timestamp(),
1163
				'page_latest'       => 0, // Fill this in shortly...
1164
				'page_len'          => 0, // Fill this in shortly...
1165
			],
1166
			__METHOD__,
1167
			'IGNORE'
1168
		);
1169
1170
		if ( $dbw->affectedRows() > 0 ) {
1171
			$newid = $pageId ?: $dbw->insertId();
1172
			$this->mId = $newid;
1173
			$this->mTitle->resetArticleID( $newid );
1174
1175
			return $newid;
1176
		} else {
1177
			return false; // nothing changed
1178
		}
1179
	}
1180
1181
	/**
1182
	 * Update the page record to point to a newly saved revision.
1183
	 *
1184
	 * @param IDatabase $dbw
1185
	 * @param Revision $revision For ID number, and text used to set
1186
	 *   length and redirect status fields
1187
	 * @param int $lastRevision If given, will not overwrite the page field
1188
	 *   when different from the currently set value.
1189
	 *   Giving 0 indicates the new page flag should be set on.
1190
	 * @param bool $lastRevIsRedirect If given, will optimize adding and
1191
	 *   removing rows in redirect table.
1192
	 * @return bool Success; false if the page row was missing or page_latest changed
1193
	 */
1194
	public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1195
		$lastRevIsRedirect = null
1196
	) {
1197
		global $wgContentHandlerUseDB;
1198
1199
		// Assertion to try to catch T92046
1200
		if ( (int)$revision->getId() === 0 ) {
1201
			throw new InvalidArgumentException(
1202
				__METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1203
			);
1204
		}
1205
1206
		$content = $revision->getContent();
1207
		$len = $content ? $content->getSize() : 0;
1208
		$rt = $content ? $content->getUltimateRedirectTarget() : null;
1209
1210
		$conditions = [ 'page_id' => $this->getId() ];
1211
1212
		if ( !is_null( $lastRevision ) ) {
1213
			// An extra check against threads stepping on each other
1214
			$conditions['page_latest'] = $lastRevision;
1215
		}
1216
1217
		$row = [ /* SET */
1218
			'page_latest'      => $revision->getId(),
1219
			'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
1220
			'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
1221
			'page_is_redirect' => $rt !== null ? 1 : 0,
1222
			'page_len'         => $len,
1223
		];
1224
1225
		if ( $wgContentHandlerUseDB ) {
1226
			$row['page_content_model'] = $revision->getContentModel();
1227
		}
1228
1229
		$dbw->update( 'page',
1230
			$row,
1231
			$conditions,
1232
			__METHOD__ );
1233
1234
		$result = $dbw->affectedRows() > 0;
1235
		if ( $result ) {
1236
			$this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
0 ignored issues
show
Bug introduced by
It seems like $rt defined by $content ? $content->get...RedirectTarget() : null on line 1208 can be null; however, WikiPage::updateRedirectOn() 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...
1237
			$this->setLastEdit( $revision );
1238
			$this->mLatest = $revision->getId();
0 ignored issues
show
Documentation Bug introduced by
It seems like $revision->getId() can also be of type integer. However, the property $mLatest is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1239
			$this->mIsRedirect = (bool)$rt;
1240
			// Update the LinkCache.
1241
			LinkCache::singleton()->addGoodLinkObj(
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1242
				$this->getId(),
1243
				$this->mTitle,
1244
				$len,
1245
				$this->mIsRedirect,
1246
				$this->mLatest,
1247
				$revision->getContentModel()
1248
			);
1249
		}
1250
1251
		return $result;
1252
	}
1253
1254
	/**
1255
	 * Add row to the redirect table if this is a redirect, remove otherwise.
1256
	 *
1257
	 * @param IDatabase $dbw
1258
	 * @param Title $redirectTitle Title object pointing to the redirect target,
1259
	 *   or NULL if this is not a redirect
1260
	 * @param null|bool $lastRevIsRedirect If given, will optimize adding and
1261
	 *   removing rows in redirect table.
1262
	 * @return bool True on success, false on failure
1263
	 * @private
1264
	 */
1265
	public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1266
		// Always update redirects (target link might have changed)
1267
		// Update/Insert if we don't know if the last revision was a redirect or not
1268
		// Delete if changing from redirect to non-redirect
1269
		$isRedirect = !is_null( $redirectTitle );
1270
1271
		if ( !$isRedirect && $lastRevIsRedirect === false ) {
1272
			return true;
1273
		}
1274
1275
		if ( $isRedirect ) {
1276
			$this->insertRedirectEntry( $redirectTitle );
1277
		} else {
1278
			// This is not a redirect, remove row from redirect table
1279
			$where = [ 'rd_from' => $this->getId() ];
1280
			$dbw->delete( 'redirect', $where, __METHOD__ );
1281
		}
1282
1283
		if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1284
			RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1285
		}
1286
1287
		return ( $dbw->affectedRows() != 0 );
1288
	}
1289
1290
	/**
1291
	 * If the given revision is newer than the currently set page_latest,
1292
	 * update the page record. Otherwise, do nothing.
1293
	 *
1294
	 * @deprecated since 1.24, use updateRevisionOn instead
1295
	 *
1296
	 * @param IDatabase $dbw
1297
	 * @param Revision $revision
1298
	 * @return bool
1299
	 */
1300
	public function updateIfNewerOn( $dbw, $revision ) {
1301
1302
		$row = $dbw->selectRow(
1303
			[ 'revision', 'page' ],
1304
			[ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1305
			[
1306
				'page_id' => $this->getId(),
1307
				'page_latest=rev_id' ],
1308
			__METHOD__ );
1309
1310
		if ( $row ) {
1311
			if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1312
				return false;
1313
			}
1314
			$prev = $row->rev_id;
1315
			$lastRevIsRedirect = (bool)$row->page_is_redirect;
1316
		} else {
1317
			// No or missing previous revision; mark the page as new
1318
			$prev = 0;
1319
			$lastRevIsRedirect = null;
1320
		}
1321
1322
		$ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1323
1324
		return $ret;
1325
	}
1326
1327
	/**
1328
	 * Get the content that needs to be saved in order to undo all revisions
1329
	 * between $undo and $undoafter. Revisions must belong to the same page,
1330
	 * must exist and must not be deleted
1331
	 * @param Revision $undo
1332
	 * @param Revision $undoafter Must be an earlier revision than $undo
1333
	 * @return Content|bool Content on success, false on failure
1334
	 * @since 1.21
1335
	 * Before we had the Content object, this was done in getUndoText
1336
	 */
1337
	public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1338
		$handler = $undo->getContentHandler();
1339
		return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
0 ignored issues
show
Bug introduced by
It seems like $this->getRevision() can be null; however, getUndoContent() 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...
Bug introduced by
It seems like $undoafter defined by parameter $undoafter on line 1337 can be null; however, ContentHandler::getUndoContent() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
1340
	}
1341
1342
	/**
1343
	 * Returns true if this page's content model supports sections.
1344
	 *
1345
	 * @return bool
1346
	 *
1347
	 * @todo The skin should check this and not offer section functionality if
1348
	 *   sections are not supported.
1349
	 * @todo The EditPage should check this and not offer section functionality
1350
	 *   if sections are not supported.
1351
	 */
1352
	public function supportsSections() {
1353
		return $this->getContentHandler()->supportsSections();
1354
	}
1355
1356
	/**
1357
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1358
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1359
	 * or 'new' for a new section.
1360
	 * @param Content $sectionContent New content of the section.
1361
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1362
	 * @param string $edittime Revision timestamp or null to use the current revision.
1363
	 *
1364
	 * @throws MWException
1365
	 * @return Content|null New complete article content, or null if error.
1366
	 *
1367
	 * @since 1.21
1368
	 * @deprecated since 1.24, use replaceSectionAtRev instead
1369
	 */
1370
	public function replaceSectionContent(
1371
		$sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1372
	) {
1373
1374
		$baseRevId = null;
1375
		if ( $edittime && $sectionId !== 'new' ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $edittime of type string|null 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...
1376
			$dbr = wfGetDB( DB_SLAVE );
1377
			$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1378
			// Try the master if this thread may have just added it.
1379
			// This could be abstracted into a Revision method, but we don't want
1380
			// to encourage loading of revisions by timestamp.
1381
			if ( !$rev
1382
				&& wfGetLB()->getServerCount() > 1
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or 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...
1383
				&& wfGetLB()->hasOrMadeRecentMasterChanges()
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or 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...
1384
			) {
1385
				$dbw = wfGetDB( DB_MASTER );
1386
				$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1387
			}
1388
			if ( $rev ) {
1389
				$baseRevId = $rev->getId();
1390
			}
1391
		}
1392
1393
		return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1394
	}
1395
1396
	/**
1397
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1398
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1399
	 * or 'new' for a new section.
1400
	 * @param Content $sectionContent New content of the section.
1401
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1402
	 * @param int|null $baseRevId
1403
	 *
1404
	 * @throws MWException
1405
	 * @return Content|null New complete article content, or null if error.
1406
	 *
1407
	 * @since 1.24
1408
	 */
1409
	public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1410
		$sectionTitle = '', $baseRevId = null
1411
	) {
1412
1413
		if ( strval( $sectionId ) === '' ) {
1414
			// Whole-page edit; let the whole text through
1415
			$newContent = $sectionContent;
1416
		} else {
1417
			if ( !$this->supportsSections() ) {
1418
				throw new MWException( "sections not supported for content model " .
1419
					$this->getContentHandler()->getModelID() );
1420
			}
1421
1422
			// Bug 30711: always use current version when adding a new section
1423
			if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1424
				$oldContent = $this->getContent();
1425
			} else {
1426
				$rev = Revision::newFromId( $baseRevId );
1427
				if ( !$rev ) {
1428
					wfDebug( __METHOD__ . " asked for bogus section (page: " .
1429
						$this->getId() . "; section: $sectionId)\n" );
1430
					return null;
1431
				}
1432
1433
				$oldContent = $rev->getContent();
1434
			}
1435
1436
			if ( !$oldContent ) {
1437
				wfDebug( __METHOD__ . ": no page text\n" );
1438
				return null;
1439
			}
1440
1441
			$newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1442
		}
1443
1444
		return $newContent;
1445
	}
1446
1447
	/**
1448
	 * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
1449
	 * @param int $flags
1450
	 * @return int Updated $flags
1451
	 */
1452
	public function checkFlags( $flags ) {
1453
		if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1454
			if ( $this->exists() ) {
1455
				$flags |= EDIT_UPDATE;
1456
			} else {
1457
				$flags |= EDIT_NEW;
1458
			}
1459
		}
1460
1461
		return $flags;
1462
	}
1463
1464
	/**
1465
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1466
	 * optionally via the deferred update array.
1467
	 *
1468
	 * @param string $text New text
1469
	 * @param string $summary Edit summary
1470
	 * @param int $flags Bitfield:
1471
	 *      EDIT_NEW
1472
	 *          Article is known or assumed to be non-existent, create a new one
1473
	 *      EDIT_UPDATE
1474
	 *          Article is known or assumed to be pre-existing, update it
1475
	 *      EDIT_MINOR
1476
	 *          Mark this edit minor, if the user is allowed to do so
1477
	 *      EDIT_SUPPRESS_RC
1478
	 *          Do not log the change in recentchanges
1479
	 *      EDIT_FORCE_BOT
1480
	 *          Mark the edit a "bot" edit regardless of user rights
1481
	 *      EDIT_AUTOSUMMARY
1482
	 *          Fill in blank summaries with generated text where possible
1483
	 *
1484
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1485
	 * article will be detected. If EDIT_UPDATE is specified and the article
1486
	 * doesn't exist, the function will return an edit-gone-missing error. If
1487
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1488
	 * error will be returned. These two conditions are also possible with
1489
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1490
	 *
1491
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1492
	 *   This is not the parent revision ID, rather the revision ID for older
1493
	 *   content used as the source for a rollback, for example.
1494
	 * @param User $user The user doing the edit
1495
	 *
1496
	 * @throws MWException
1497
	 * @return Status Possible errors:
1498
	 *   edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1499
	 *     set the fatal flag of $status
1500
	 *   edit-gone-missing: In update mode, but the article didn't exist.
1501
	 *   edit-conflict: In update mode, the article changed unexpectedly.
1502
	 *   edit-no-change: Warning that the text was the same as before.
1503
	 *   edit-already-exists: In creation mode, but the article already exists.
1504
	 *
1505
	 * Extensions may define additional errors.
1506
	 *
1507
	 * $return->value will contain an associative array with members as follows:
1508
	 *     new: Boolean indicating if the function attempted to create a new article.
1509
	 *     revision: The revision object for the inserted revision, or null.
1510
	 *
1511
	 * Compatibility note: this function previously returned a boolean value
1512
	 * indicating success/failure
1513
	 *
1514
	 * @deprecated since 1.21: use doEditContent() instead.
1515
	 */
1516
	public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1517
		ContentHandler::deprecated( __METHOD__, '1.21' );
1518
1519
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
1520
1521
		return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1522
	}
1523
1524
	/**
1525
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1526
	 * optionally via the deferred update array.
1527
	 *
1528
	 * @param Content $content New content
1529
	 * @param string $summary Edit summary
1530
	 * @param int $flags Bitfield:
1531
	 *      EDIT_NEW
1532
	 *          Article is known or assumed to be non-existent, create a new one
1533
	 *      EDIT_UPDATE
1534
	 *          Article is known or assumed to be pre-existing, update it
1535
	 *      EDIT_MINOR
1536
	 *          Mark this edit minor, if the user is allowed to do so
1537
	 *      EDIT_SUPPRESS_RC
1538
	 *          Do not log the change in recentchanges
1539
	 *      EDIT_FORCE_BOT
1540
	 *          Mark the edit a "bot" edit regardless of user rights
1541
	 *      EDIT_AUTOSUMMARY
1542
	 *          Fill in blank summaries with generated text where possible
1543
	 *
1544
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1545
	 * article will be detected. If EDIT_UPDATE is specified and the article
1546
	 * doesn't exist, the function will return an edit-gone-missing error. If
1547
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1548
	 * error will be returned. These two conditions are also possible with
1549
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1550
	 *
1551
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1552
	 *   This is not the parent revision ID, rather the revision ID for older
1553
	 *   content used as the source for a rollback, for example.
1554
	 * @param User $user The user doing the edit
1555
	 * @param string $serialFormat Format for storing the content in the
1556
	 *   database.
1557
	 * @param array|null $tags Change tags to apply to this edit
1558
	 * Callers are responsible for permission checks
1559
	 * (with ChangeTags::canAddTagsAccompanyingChange)
1560
	 *
1561
	 * @throws MWException
1562
	 * @return Status Possible errors:
1563
	 *     edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1564
	 *       set the fatal flag of $status.
1565
	 *     edit-gone-missing: In update mode, but the article didn't exist.
1566
	 *     edit-conflict: In update mode, the article changed unexpectedly.
1567
	 *     edit-no-change: Warning that the text was the same as before.
1568
	 *     edit-already-exists: In creation mode, but the article already exists.
1569
	 *
1570
	 *  Extensions may define additional errors.
1571
	 *
1572
	 *  $return->value will contain an associative array with members as follows:
1573
	 *     new: Boolean indicating if the function attempted to create a new article.
1574
	 *     revision: The revision object for the inserted revision, or null.
1575
	 *
1576
	 * @since 1.21
1577
	 * @throws MWException
1578
	 */
1579
	public function doEditContent(
1580
		Content $content, $summary, $flags = 0, $baseRevId = false,
1581
		User $user = null, $serialFormat = null, $tags = null
1582
	) {
1583
		global $wgUser, $wgUseAutomaticEditSummaries;
1584
1585
		// Low-level sanity check
1586
		if ( $this->mTitle->getText() === '' ) {
1587
			throw new MWException( 'Something is trying to edit an article with an empty title' );
1588
		}
1589
		// Make sure the given content type is allowed for this page
1590
		if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1591
			return Status::newFatal( 'content-not-allowed-here',
1592
				ContentHandler::getLocalizedName( $content->getModel() ),
1593
				$this->mTitle->getPrefixedText()
1594
			);
1595
		}
1596
1597
		// Load the data from the master database if needed.
1598
		// The caller may already loaded it from the master or even loaded it using
1599
		// SELECT FOR UPDATE, so do not override that using clear().
1600
		$this->loadPageData( 'fromdbmaster' );
1601
1602
		$user = $user ?: $wgUser;
1603
		$flags = $this->checkFlags( $flags );
1604
1605
		// Trigger pre-save hook (using provided edit summary)
1606
		$hookStatus = Status::newGood( [] );
1607
		$hook_args = [ &$this, &$user, &$content, &$summary,
1608
							$flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1609
		// Check if the hook rejected the attempted save
1610
		if ( !Hooks::run( 'PageContentSave', $hook_args )
1611
			|| !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
1612
		) {
1613
			if ( $hookStatus->isOK() ) {
1614
				// Hook returned false but didn't call fatal(); use generic message
1615
				$hookStatus->fatal( 'edit-hook-aborted' );
1616
			}
1617
1618
			return $hookStatus;
1619
		}
1620
1621
		$old_revision = $this->getRevision(); // current revision
1622
		$old_content = $this->getContent( Revision::RAW ); // current revision's content
1623
1624
		// Provide autosummaries if one is not provided and autosummaries are enabled
1625
		if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1626
			$handler = $content->getContentHandler();
1627
			$summary = $handler->getAutosummary( $old_content, $content, $flags );
1628
		}
1629
1630
		// Get the pre-save transform content and final parser output
1631
		$editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat );
1632
		$pstContent = $editInfo->pstContent; // Content object
1633
		$meta = [
1634
			'bot' => ( $flags & EDIT_FORCE_BOT ),
1635
			'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1636
			'serialized' => $editInfo->pst,
1637
			'serialFormat' => $serialFormat,
1638
			'baseRevId' => $baseRevId,
1639
			'oldRevision' => $old_revision,
1640
			'oldContent' => $old_content,
1641
			'oldId' => $this->getLatest(),
1642
			'oldIsRedirect' => $this->isRedirect(),
1643
			'oldCountable' => $this->isCountable(),
1644
			'tags' => ( $tags !== null ) ? (array)$tags : []
1645
		];
1646
1647
		// Actually create the revision and create/update the page
1648
		if ( $flags & EDIT_UPDATE ) {
1649
			$status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1650
		} else {
1651
			$status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1652
		}
1653
1654
		// Promote user to any groups they meet the criteria for
1655
		DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1656
			$user->addAutopromoteOnceGroups( 'onEdit' );
1657
			$user->addAutopromoteOnceGroups( 'onView' ); // b/c
1658
		} );
1659
1660
		return $status;
1661
	}
1662
1663
	/**
1664
	 * @param Content $content Pre-save transform content
1665
	 * @param integer $flags
1666
	 * @param User $user
1667
	 * @param string $summary
1668
	 * @param array $meta
1669
	 * @return Status
1670
	 * @throws DBUnexpectedError
1671
	 * @throws Exception
1672
	 * @throws FatalError
1673
	 * @throws MWException
1674
	 */
1675
	private function doModify(
1676
		Content $content, $flags, User $user, $summary, array $meta
1677
	) {
1678
		global $wgUseRCPatrol;
1679
1680
		// Update article, but only if changed.
1681
		$status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1682
1683
		// Convenience variables
1684
		$now = wfTimestampNow();
1685
		$oldid = $meta['oldId'];
1686
		/** @var $oldContent Content|null */
1687
		$oldContent = $meta['oldContent'];
1688
		$newsize = $content->getSize();
1689
1690
		if ( !$oldid ) {
1691
			// Article gone missing
1692
			$status->fatal( 'edit-gone-missing' );
1693
1694
			return $status;
1695
		} elseif ( !$oldContent ) {
1696
			// Sanity check for bug 37225
1697
			throw new MWException( "Could not find text for current revision {$oldid}." );
1698
		}
1699
1700
		// @TODO: pass content object?!
1701
		$revision = new Revision( [
1702
			'page'       => $this->getId(),
1703
			'title'      => $this->mTitle, // for determining the default content model
1704
			'comment'    => $summary,
1705
			'minor_edit' => $meta['minor'],
1706
			'text'       => $meta['serialized'],
1707
			'len'        => $newsize,
1708
			'parent_id'  => $oldid,
1709
			'user'       => $user->getId(),
1710
			'user_text'  => $user->getName(),
1711
			'timestamp'  => $now,
1712
			'content_model' => $content->getModel(),
1713
			'content_format' => $meta['serialFormat'],
1714
		] );
1715
1716
		$changed = !$content->equals( $oldContent );
1717
1718
		$dbw = wfGetDB( DB_MASTER );
1719
1720
		if ( $changed ) {
1721
			$prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1722
			$status->merge( $prepStatus );
1723
			if ( !$status->isOK() ) {
1724
				return $status;
1725
			}
1726
1727
			$dbw->startAtomic( __METHOD__ );
1728
			// Get the latest page_latest value while locking it.
1729
			// Do a CAS style check to see if it's the same as when this method
1730
			// started. If it changed then bail out before touching the DB.
1731
			$latestNow = $this->lockAndGetLatest();
1732
			if ( $latestNow != $oldid ) {
1733
				$dbw->endAtomic( __METHOD__ );
1734
				// Page updated or deleted in the mean time
1735
				$status->fatal( 'edit-conflict' );
1736
1737
				return $status;
1738
			}
1739
1740
			// At this point we are now comitted to returning an OK
1741
			// status unless some DB query error or other exception comes up.
1742
			// This way callers don't have to call rollback() if $status is bad
1743
			// unless they actually try to catch exceptions (which is rare).
1744
1745
			// Save the revision text
1746
			$revisionId = $revision->insertOn( $dbw );
1747
			// Update page_latest and friends to reflect the new revision
1748
			if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1749
				$dbw->rollback( __METHOD__ ); // sanity; this should never happen
1750
				throw new MWException( "Failed to update page row to use new revision." );
1751
			}
1752
1753
			Hooks::run( 'NewRevisionFromEditComplete',
1754
				[ $this, $revision, $meta['baseRevId'], $user ] );
1755
1756
			// Update recentchanges
1757
			if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1758
				// Mark as patrolled if the user can do so
1759
				$patrolled = $wgUseRCPatrol && !count(
1760
						$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1761
				// Add RC row to the DB
1762
				RecentChange::notifyEdit(
1763
					$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1684 can also be of type false; however, RecentChange::notifyEdit() 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...
1764
					$this->mTitle,
1765
					$revision->isMinor(),
1766
					$user,
1767
					$summary,
1768
					$oldid,
1769
					$this->getTimestamp(),
0 ignored issues
show
Security Bug introduced by
It seems like $this->getTimestamp() targeting WikiPage::getTimestamp() can also be of type false; however, RecentChange::notifyEdit() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1770
					$meta['bot'],
1771
					'',
1772
					$oldContent ? $oldContent->getSize() : 0,
1773
					$newsize,
1774
					$revisionId,
1775
					$patrolled,
1776
					$meta['tags']
1777
				);
1778
			}
1779
1780
			$user->incEditCount();
1781
1782
			$dbw->endAtomic( __METHOD__ );
1783
			$this->mTimestamp = $now;
0 ignored issues
show
Documentation Bug introduced by
It seems like $now can also be of type false. However, the property $mTimestamp is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1784
		} else {
1785
			// Bug 32948: revision ID must be set to page {{REVISIONID}} and
1786
			// related variables correctly
1787
			$revision->setId( $this->getLatest() );
1788
		}
1789
1790
		if ( $changed ) {
1791
			// Return the new revision to the caller
1792
			$status->value['revision'] = $revision;
1793
		} else {
1794
			$status->warning( 'edit-no-change' );
1795
			// Update page_touched as updateRevisionOn() was not called.
1796
			// Other cache updates are managed in onArticleEdit() via doEditUpdates().
1797
			$this->mTitle->invalidateCache( $now );
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1684 can also be of type false; however, Title::invalidateCache() does only seem to accept string|null, 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...
1798
		}
1799
1800
		// Do secondary updates once the main changes have been committed...
1801
		$that = $this;
1802
		$dbw->onTransactionIdle(
1803
			function () use (
1804
				$dbw, &$that, $revision, &$user, $content, $summary, &$flags,
1805
				$changed, $meta, &$status
1806
			) {
1807
				// Do per-page updates in a transaction
1808
				$dbw->setFlag( DBO_TRX );
1809
				// Update links tables, site stats, etc.
1810
				$that->doEditUpdates(
1811
					$revision,
1812
					$user,
1813
					[
1814
						'changed' => $changed,
1815
						'oldcountable' => $meta['oldCountable'],
1816
						'oldrevision' => $meta['oldRevision']
1817
					]
1818
				);
1819
				// Trigger post-save hook
1820
				$params = [ &$that, &$user, $content, $summary, $flags & EDIT_MINOR,
1821
					null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
1822
				ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1823
				Hooks::run( 'PageContentSaveComplete', $params );
1824
			}
1825
		);
1826
1827
		return $status;
1828
	}
1829
1830
	/**
1831
	 * @param Content $content Pre-save transform content
1832
	 * @param integer $flags
1833
	 * @param User $user
1834
	 * @param string $summary
1835
	 * @param array $meta
1836
	 * @return Status
1837
	 * @throws DBUnexpectedError
1838
	 * @throws Exception
1839
	 * @throws FatalError
1840
	 * @throws MWException
1841
	 */
1842
	private function doCreate(
1843
		Content $content, $flags, User $user, $summary, array $meta
1844
	) {
1845
		global $wgUseRCPatrol, $wgUseNPPatrol;
1846
1847
		$status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1848
1849
		$now = wfTimestampNow();
1850
		$newsize = $content->getSize();
1851
		$prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1852
		$status->merge( $prepStatus );
1853
		if ( !$status->isOK() ) {
1854
			return $status;
1855
		}
1856
1857
		$dbw = wfGetDB( DB_MASTER );
1858
		$dbw->startAtomic( __METHOD__ );
1859
1860
		// Add the page record unless one already exists for the title
1861
		$newid = $this->insertOn( $dbw );
1862
		if ( $newid === false ) {
1863
			$dbw->endAtomic( __METHOD__ ); // nothing inserted
1864
			$status->fatal( 'edit-already-exists' );
1865
1866
			return $status; // nothing done
1867
		}
1868
1869
		// At this point we are now comitted to returning an OK
1870
		// status unless some DB query error or other exception comes up.
1871
		// This way callers don't have to call rollback() if $status is bad
1872
		// unless they actually try to catch exceptions (which is rare).
1873
1874
		// @TODO: pass content object?!
1875
		$revision = new Revision( [
1876
			'page'       => $newid,
1877
			'title'      => $this->mTitle, // for determining the default content model
1878
			'comment'    => $summary,
1879
			'minor_edit' => $meta['minor'],
1880
			'text'       => $meta['serialized'],
1881
			'len'        => $newsize,
1882
			'user'       => $user->getId(),
1883
			'user_text'  => $user->getName(),
1884
			'timestamp'  => $now,
1885
			'content_model' => $content->getModel(),
1886
			'content_format' => $meta['serialFormat'],
1887
		] );
1888
1889
		// Save the revision text...
1890
		$revisionId = $revision->insertOn( $dbw );
1891
		// Update the page record with revision data
1892
		if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1893
			$dbw->rollback( __METHOD__ ); // sanity; this should never happen
1894
			throw new MWException( "Failed to update page row to use new revision." );
1895
		}
1896
1897
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1898
1899
		// Update recentchanges
1900
		if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1901
			// Mark as patrolled if the user can do so
1902
			$patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1903
				!count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1904
			// Add RC row to the DB
1905
			RecentChange::notifyNew(
1906
				$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1849 can also be of type false; however, RecentChange::notifyNew() 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...
1907
				$this->mTitle,
1908
				$revision->isMinor(),
1909
				$user,
1910
				$summary,
1911
				$meta['bot'],
1912
				'',
1913
				$newsize,
1914
				$revisionId,
1915
				$patrolled,
1916
				$meta['tags']
1917
			);
1918
		}
1919
1920
		$user->incEditCount();
1921
1922
		$dbw->endAtomic( __METHOD__ );
1923
		$this->mTimestamp = $now;
0 ignored issues
show
Documentation Bug introduced by
It seems like $now can also be of type false. However, the property $mTimestamp is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1924
1925
		// Return the new revision to the caller
1926
		$status->value['revision'] = $revision;
1927
1928
		// Do secondary updates once the main changes have been committed...
1929
		$that = $this;
1930
		$dbw->onTransactionIdle(
1931
			function () use (
1932
				&$that, $dbw, $revision, &$user, $content, $summary, &$flags, $meta, &$status
1933
			) {
1934
				// Do per-page updates in a transaction
1935
				$dbw->setFlag( DBO_TRX );
1936
				// Update links, etc.
1937
				$that->doEditUpdates( $revision, $user, [ 'created' => true ] );
1938
				// Trigger post-create hook
1939
				$params = [ &$that, &$user, $content, $summary,
1940
					$flags & EDIT_MINOR, null, null, &$flags, $revision ];
1941
				ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
1942
				Hooks::run( 'PageContentInsertComplete', $params );
1943
				// Trigger post-save hook
1944
				$params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
1945
				ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1946
				Hooks::run( 'PageContentSaveComplete', $params );
1947
1948
			}
1949
		);
1950
1951
		return $status;
1952
	}
1953
1954
	/**
1955
	 * Get parser options suitable for rendering the primary article wikitext
1956
	 *
1957
	 * @see ContentHandler::makeParserOptions
1958
	 *
1959
	 * @param IContextSource|User|string $context One of the following:
1960
	 *        - IContextSource: Use the User and the Language of the provided
1961
	 *          context
1962
	 *        - User: Use the provided User object and $wgLang for the language,
1963
	 *          so use an IContextSource object if possible.
1964
	 *        - 'canonical': Canonical options (anonymous user with default
1965
	 *          preferences and content language).
1966
	 * @return ParserOptions
1967
	 */
1968
	public function makeParserOptions( $context ) {
1969
		$options = $this->getContentHandler()->makeParserOptions( $context );
1970
1971
		if ( $this->getTitle()->isConversionTable() ) {
1972
			// @todo ConversionTable should become a separate content model, so
1973
			// we don't need special cases like this one.
1974
			$options->disableContentConversion();
1975
		}
1976
1977
		return $options;
1978
	}
1979
1980
	/**
1981
	 * Prepare text which is about to be saved.
1982
	 * Returns a stdClass with source, pst and output members
1983
	 *
1984
	 * @param string $text
1985
	 * @param int|null $revid
1986
	 * @param User|null $user
1987
	 * @deprecated since 1.21: use prepareContentForEdit instead.
1988
	 * @return object
1989
	 */
1990
	public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
1991
		ContentHandler::deprecated( __METHOD__, '1.21' );
1992
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
1993
		return $this->prepareContentForEdit( $content, $revid, $user );
1994
	}
1995
1996
	/**
1997
	 * Prepare content which is about to be saved.
1998
	 * Returns a stdClass with source, pst and output members
1999
	 *
2000
	 * @param Content $content
2001
	 * @param Revision|int|null $revision Revision object. For backwards compatibility, a
2002
	 *        revision ID is also accepted, but this is deprecated.
2003
	 * @param User|null $user
2004
	 * @param string|null $serialFormat
2005
	 * @param bool $useCache Check shared prepared edit cache
2006
	 *
2007
	 * @return object
2008
	 *
2009
	 * @since 1.21
2010
	 */
2011
	public function prepareContentForEdit(
2012
		Content $content, $revision = null, User $user = null,
2013
		$serialFormat = null, $useCache = true
2014
	) {
2015
		global $wgContLang, $wgUser, $wgAjaxEditStash;
2016
2017
		if ( is_object( $revision ) ) {
2018
			$revid = $revision->getId();
2019
		} else {
2020
			$revid = $revision;
2021
			// This code path is deprecated, and nothing is known to
2022
			// use it, so performance here shouldn't be a worry.
2023
			if ( $revid !== null ) {
2024
				$revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2025
			} else {
2026
				$revision = null;
2027
			}
2028
		}
2029
2030
		$user = is_null( $user ) ? $wgUser : $user;
2031
		// XXX: check $user->getId() here???
2032
2033
		// Use a sane default for $serialFormat, see bug 57026
2034
		if ( $serialFormat === null ) {
2035
			$serialFormat = $content->getContentHandler()->getDefaultFormat();
2036
		}
2037
2038
		if ( $this->mPreparedEdit
2039
			&& $this->mPreparedEdit->newContent
2040
			&& $this->mPreparedEdit->newContent->equals( $content )
2041
			&& $this->mPreparedEdit->revid == $revid
2042
			&& $this->mPreparedEdit->format == $serialFormat
2043
			// XXX: also check $user here?
2044
		) {
2045
			// Already prepared
2046
			return $this->mPreparedEdit;
2047
		}
2048
2049
		// The edit may have already been prepared via api.php?action=stashedit
2050
		$cachedEdit = $useCache && $wgAjaxEditStash
2051
			? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2052
			: false;
2053
2054
		$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2055
		Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2056
2057
		$edit = (object)[];
2058
		if ( $cachedEdit ) {
2059
			$edit->timestamp = $cachedEdit->timestamp;
2060
		} else {
2061
			$edit->timestamp = wfTimestampNow();
2062
		}
2063
		// @note: $cachedEdit is not used if the rev ID was referenced in the text
2064
		$edit->revid = $revid;
2065
2066
		if ( $cachedEdit ) {
2067
			$edit->pstContent = $cachedEdit->pstContent;
2068
		} else {
2069
			$edit->pstContent = $content
2070
				? $content->preSaveTransform( $this->mTitle, $user, $popts )
2071
				: null;
2072
		}
2073
2074
		$edit->format = $serialFormat;
2075
		$edit->popts = $this->makeParserOptions( 'canonical' );
2076
		if ( $cachedEdit ) {
2077
			$edit->output = $cachedEdit->output;
2078
		} else {
2079
			if ( $revision ) {
2080
				// We get here if vary-revision is set. This means that this page references
2081
				// itself (such as via self-transclusion). In this case, we need to make sure
2082
				// that any such self-references refer to the newly-saved revision, and not
2083
				// to the previous one, which could otherwise happen due to slave lag.
2084
				$oldCallback = $edit->popts->getCurrentRevisionCallback();
2085
				$edit->popts->setCurrentRevisionCallback(
2086
					function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2087
						if ( $title->equals( $revision->getTitle() ) ) {
2088
							return $revision;
2089
						} else {
2090
							return call_user_func( $oldCallback, $title, $parser );
2091
						}
2092
					}
2093
				);
2094
			}
2095
			$edit->output = $edit->pstContent
2096
				? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2097
				: null;
2098
		}
2099
2100
		$edit->newContent = $content;
2101
		$edit->oldContent = $this->getContent( Revision::RAW );
2102
2103
		// NOTE: B/C for hooks! don't use these fields!
2104
		$edit->newText = $edit->newContent
2105
			? ContentHandler::getContentText( $edit->newContent )
2106
			: '';
2107
		$edit->oldText = $edit->oldContent
2108
			? ContentHandler::getContentText( $edit->oldContent )
2109
			: '';
2110
		$edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2111
2112
		if ( $edit->output ) {
2113
			$edit->output->setCacheTime( wfTimestampNow() );
2114
		}
2115
2116
		// Process cache the result
2117
		$this->mPreparedEdit = $edit;
2118
2119
		return $edit;
2120
	}
2121
2122
	/**
2123
	 * Do standard deferred updates after page edit.
2124
	 * Update links tables, site stats, search index and message cache.
2125
	 * Purges pages that include this page if the text was changed here.
2126
	 * Every 100th edit, prune the recent changes table.
2127
	 *
2128
	 * @param Revision $revision
2129
	 * @param User $user User object that did the revision
2130
	 * @param array $options Array of options, following indexes are used:
2131
	 * - changed: boolean, whether the revision changed the content (default true)
2132
	 * - created: boolean, whether the revision created the page (default false)
2133
	 * - moved: boolean, whether the page was moved (default false)
2134
	 * - restored: boolean, whether the page was undeleted (default false)
2135
	 * - oldrevision: Revision object for the pre-update revision (default null)
2136
	 * - oldcountable: boolean, null, or string 'no-change' (default null):
2137
	 *   - boolean: whether the page was counted as an article before that
2138
	 *     revision, only used in changed is true and created is false
2139
	 *   - null: if created is false, don't update the article count; if created
2140
	 *     is true, do update the article count
2141
	 *   - 'no-change': don't update the article count, ever
2142
	 */
2143
	public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2144
		global $wgRCWatchCategoryMembership, $wgContLang;
2145
2146
		$options += [
2147
			'changed' => true,
2148
			'created' => false,
2149
			'moved' => false,
2150
			'restored' => false,
2151
			'oldrevision' => null,
2152
			'oldcountable' => null
2153
		];
2154
		$content = $revision->getContent();
2155
2156
		// Parse the text
2157
		// Be careful not to do pre-save transform twice: $text is usually
2158
		// already pre-save transformed once.
2159
		if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2160
			wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
2161
			$editInfo = $this->prepareContentForEdit( $content, $revision, $user );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2154 can be null; however, WikiPage::prepareContentForEdit() 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...
2162
		} else {
2163
			wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
2164
			$editInfo = $this->mPreparedEdit;
2165
		}
2166
2167
		// Save it to the parser cache.
2168
		// Make sure the cache time matches page_touched to avoid double parsing.
2169
		ParserCache::singleton()->save(
2170
			$editInfo->output, $this, $editInfo->popts,
2171
			$revision->getTimestamp(), $editInfo->revid
0 ignored issues
show
Security Bug introduced by
It seems like $revision->getTimestamp() targeting Revision::getTimestamp() can also be of type false; however, ParserCache::save() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
2172
		);
2173
2174
		// Update the links tables and other secondary data
2175
		if ( $content ) {
2176
			$recursive = $options['changed']; // bug 50785
2177
			$updates = $content->getSecondaryDataUpdates(
2178
				$this->getTitle(), null, $recursive, $editInfo->output
2179
			);
2180
			foreach ( $updates as $update ) {
2181
				if ( $update instanceof LinksUpdate ) {
2182
					$update->setRevision( $revision );
2183
					$update->setTriggeringUser( $user );
2184
				}
2185
				DeferredUpdates::addUpdate( $update );
2186
			}
2187
			if ( $wgRCWatchCategoryMembership
2188
				&& $this->getContentHandler()->supportsCategories() === true
2189
				&& ( $options['changed'] || $options['created'] )
2190
				&& !$options['restored']
2191
			) {
2192
				// Note: jobs are pushed after deferred updates, so the job should be able to see
2193
				// the recent change entry (also done via deferred updates) and carry over any
2194
				// bot/deletion/IP flags, ect.
2195
				JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
2196
					$this->getTitle(),
2197
					[
2198
						'pageId' => $this->getId(),
2199
						'revTimestamp' => $revision->getTimestamp()
2200
					]
2201
				) );
2202
			}
2203
		}
2204
2205
		Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
2206
2207
		if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
2208
			// Flush old entries from the `recentchanges` table
2209
			if ( mt_rand( 0, 9 ) == 0 ) {
2210
				JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
2211
			}
2212
		}
2213
2214
		if ( !$this->exists() ) {
2215
			return;
2216
		}
2217
2218
		$id = $this->getId();
2219
		$title = $this->mTitle->getPrefixedDBkey();
2220
		$shortTitle = $this->mTitle->getDBkey();
2221
2222
		if ( $options['oldcountable'] === 'no-change' ||
2223
			( !$options['changed'] && !$options['moved'] )
2224
		) {
2225
			$good = 0;
2226
		} elseif ( $options['created'] ) {
2227
			$good = (int)$this->isCountable( $editInfo );
2228
		} elseif ( $options['oldcountable'] !== null ) {
2229
			$good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2230
		} else {
2231
			$good = 0;
2232
		}
2233
		$edits = $options['changed'] ? 1 : 0;
2234
		$total = $options['created'] ? 1 : 0;
2235
2236
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2237
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2154 can be null; however, SearchUpdate::__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...
2238
2239
		// If this is another user's talk page, update newtalk.
2240
		// Don't do this if $options['changed'] = false (null-edits) nor if
2241
		// it's a minor edit and the user doesn't want notifications for those.
2242
		if ( $options['changed']
2243
			&& $this->mTitle->getNamespace() == NS_USER_TALK
2244
			&& $shortTitle != $user->getTitleKey()
2245
			&& !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2246
		) {
2247
			$recipient = User::newFromName( $shortTitle, false );
2248
			if ( !$recipient ) {
2249
				wfDebug( __METHOD__ . ": invalid username\n" );
2250
			} else {
2251
				// Allow extensions to prevent user notification
2252
				// when a new message is added to their talk page
2253
				if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
2254
					if ( User::isIP( $shortTitle ) ) {
2255
						// An anonymous user
2256
						$recipient->setNewtalk( true, $revision );
2257
					} elseif ( $recipient->isLoggedIn() ) {
2258
						$recipient->setNewtalk( true, $revision );
2259
					} else {
2260
						wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2261
					}
2262
				}
2263
			}
2264
		}
2265
2266
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2267
			// XXX: could skip pseudo-messages like js/css here, based on content model.
2268
			$msgtext = $content ? $content->getWikitextForTransclusion() : null;
2269
			if ( $msgtext === false || $msgtext === null ) {
2270
				$msgtext = '';
2271
			}
2272
2273
			MessageCache::singleton()->replace( $shortTitle, $msgtext );
2274
2275
			if ( $wgContLang->hasVariants() ) {
2276
				$wgContLang->updateConversionTable( $this->mTitle );
2277
			}
2278
		}
2279
2280
		if ( $options['created'] ) {
2281
			self::onArticleCreate( $this->mTitle );
2282
		} elseif ( $options['changed'] ) { // bug 50785
2283
			self::onArticleEdit( $this->mTitle, $revision );
2284
		}
2285
	}
2286
2287
	/**
2288
	 * Edit an article without doing all that other stuff
2289
	 * The article must already exist; link tables etc
2290
	 * are not updated, caches are not flushed.
2291
	 *
2292
	 * @param Content $content Content submitted
2293
	 * @param User $user The relevant user
2294
	 * @param string $comment Comment submitted
2295
	 * @param bool $minor Whereas it's a minor modification
2296
	 * @param string $serialFormat Format for storing the content in the database
2297
	 */
2298
	public function doQuickEditContent(
2299
		Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
2300
	) {
2301
2302
		$serialized = $content->serialize( $serialFormat );
2303
2304
		$dbw = wfGetDB( DB_MASTER );
2305
		$revision = new Revision( [
2306
			'title'      => $this->getTitle(), // for determining the default content model
2307
			'page'       => $this->getId(),
2308
			'user_text'  => $user->getName(),
2309
			'user'       => $user->getId(),
2310
			'text'       => $serialized,
2311
			'length'     => $content->getSize(),
2312
			'comment'    => $comment,
2313
			'minor_edit' => $minor ? 1 : 0,
2314
		] ); // XXX: set the content object?
2315
		$revision->insertOn( $dbw );
2316
		$this->updateRevisionOn( $dbw, $revision );
2317
2318
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
2319
2320
	}
2321
2322
	/**
2323
	 * Update the article's restriction field, and leave a log entry.
2324
	 * This works for protection both existing and non-existing pages.
2325
	 *
2326
	 * @param array $limit Set of restriction keys
2327
	 * @param array $expiry Per restriction type expiration
2328
	 * @param int &$cascade Set to false if cascading protection isn't allowed.
2329
	 * @param string $reason
2330
	 * @param User $user The user updating the restrictions
2331
	 * @param string|string[] $tags Change tags to add to the pages and protection log entries
2332
	 *   ($user should be able to add the specified tags before this is called)
2333
	 * @return Status Status object; if action is taken, $status->value is the log_id of the
2334
	 *   protection log entry.
2335
	 */
2336
	public function doUpdateRestrictions( array $limit, array $expiry,
2337
		&$cascade, $reason, User $user, $tags = null
2338
	) {
2339
		global $wgCascadingRestrictionLevels, $wgContLang;
2340
2341
		if ( wfReadOnly() ) {
2342
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2343
		}
2344
2345
		$this->loadPageData( 'fromdbmaster' );
2346
		$restrictionTypes = $this->mTitle->getRestrictionTypes();
2347
		$id = $this->getId();
2348
2349
		if ( !$cascade ) {
2350
			$cascade = false;
2351
		}
2352
2353
		// Take this opportunity to purge out expired restrictions
2354
		Title::purgeExpiredRestrictions();
2355
2356
		// @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2357
		// we expect a single selection, but the schema allows otherwise.
2358
		$isProtected = false;
2359
		$protect = false;
2360
		$changed = false;
2361
2362
		$dbw = wfGetDB( DB_MASTER );
2363
2364
		foreach ( $restrictionTypes as $action ) {
2365
			if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2366
				$expiry[$action] = 'infinity';
2367
			}
2368
			if ( !isset( $limit[$action] ) ) {
2369
				$limit[$action] = '';
2370
			} elseif ( $limit[$action] != '' ) {
2371
				$protect = true;
2372
			}
2373
2374
			// Get current restrictions on $action
2375
			$current = implode( '', $this->mTitle->getRestrictions( $action ) );
2376
			if ( $current != '' ) {
2377
				$isProtected = true;
2378
			}
2379
2380
			if ( $limit[$action] != $current ) {
2381
				$changed = true;
2382
			} elseif ( $limit[$action] != '' ) {
2383
				// Only check expiry change if the action is actually being
2384
				// protected, since expiry does nothing on an not-protected
2385
				// action.
2386
				if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2387
					$changed = true;
2388
				}
2389
			}
2390
		}
2391
2392
		if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2393
			$changed = true;
2394
		}
2395
2396
		// If nothing has changed, do nothing
2397
		if ( !$changed ) {
2398
			return Status::newGood();
2399
		}
2400
2401
		if ( !$protect ) { // No protection at all means unprotection
2402
			$revCommentMsg = 'unprotectedarticle';
2403
			$logAction = 'unprotect';
2404
		} elseif ( $isProtected ) {
2405
			$revCommentMsg = 'modifiedarticleprotection';
2406
			$logAction = 'modify';
2407
		} else {
2408
			$revCommentMsg = 'protectedarticle';
2409
			$logAction = 'protect';
2410
		}
2411
2412
		// Truncate for whole multibyte characters
2413
		$reason = $wgContLang->truncate( $reason, 255 );
2414
2415
		$logRelationsValues = [];
2416
		$logRelationsField = null;
2417
		$logParamsDetails = [];
2418
2419
		// Null revision (used for change tag insertion)
2420
		$nullRevision = null;
2421
2422
		if ( $id ) { // Protection of existing page
2423
			if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
2424
				return Status::newGood();
2425
			}
2426
2427
			// Only certain restrictions can cascade...
2428
			$editrestriction = isset( $limit['edit'] )
2429
				? [ $limit['edit'] ]
2430
				: $this->mTitle->getRestrictions( 'edit' );
2431
			foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2432
				$editrestriction[$key] = 'editprotected'; // backwards compatibility
2433
			}
2434
			foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2435
				$editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2436
			}
2437
2438
			$cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2439
			foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2440
				$cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2441
			}
2442
			foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2443
				$cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2444
			}
2445
2446
			// The schema allows multiple restrictions
2447
			if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2448
				$cascade = false;
2449
			}
2450
2451
			// insert null revision to identify the page protection change as edit summary
2452
			$latest = $this->getLatest();
2453
			$nullRevision = $this->insertProtectNullRevision(
2454
				$revCommentMsg,
2455
				$limit,
2456
				$expiry,
2457
				$cascade,
2458
				$reason,
2459
				$user
2460
			);
2461
2462
			if ( $nullRevision === null ) {
2463
				return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2464
			}
2465
2466
			$logRelationsField = 'pr_id';
2467
2468
			// Update restrictions table
2469
			foreach ( $limit as $action => $restrictions ) {
2470
				$dbw->delete(
2471
					'page_restrictions',
2472
					[
2473
						'pr_page' => $id,
2474
						'pr_type' => $action
2475
					],
2476
					__METHOD__
2477
				);
2478
				if ( $restrictions != '' ) {
2479
					$cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2480
					$dbw->insert(
2481
						'page_restrictions',
2482
						[
2483
							'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2484
							'pr_page' => $id,
2485
							'pr_type' => $action,
2486
							'pr_level' => $restrictions,
2487
							'pr_cascade' => $cascadeValue,
2488
							'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2489
						],
2490
						__METHOD__
2491
					);
2492
					$logRelationsValues[] = $dbw->insertId();
2493
					$logParamsDetails[] = [
2494
						'type' => $action,
2495
						'level' => $restrictions,
2496
						'expiry' => $expiry[$action],
2497
						'cascade' => (bool)$cascadeValue,
2498
					];
2499
				}
2500
			}
2501
2502
			// Clear out legacy restriction fields
2503
			$dbw->update(
2504
				'page',
2505
				[ 'page_restrictions' => '' ],
2506
				[ 'page_id' => $id ],
2507
				__METHOD__
2508
			);
2509
2510
			Hooks::run( 'NewRevisionFromEditComplete',
2511
				[ $this, $nullRevision, $latest, $user ] );
2512
			Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
2513
		} else { // Protection of non-existing page (also known as "title protection")
2514
			// Cascade protection is meaningless in this case
2515
			$cascade = false;
2516
2517
			if ( $limit['create'] != '' ) {
2518
				$dbw->replace( 'protected_titles',
2519
					[ [ 'pt_namespace', 'pt_title' ] ],
2520
					[
2521
						'pt_namespace' => $this->mTitle->getNamespace(),
2522
						'pt_title' => $this->mTitle->getDBkey(),
2523
						'pt_create_perm' => $limit['create'],
2524
						'pt_timestamp' => $dbw->timestamp(),
2525
						'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2526
						'pt_user' => $user->getId(),
2527
						'pt_reason' => $reason,
2528
					], __METHOD__
2529
				);
2530
				$logParamsDetails[] = [
2531
					'type' => 'create',
2532
					'level' => $limit['create'],
2533
					'expiry' => $expiry['create'],
2534
				];
2535
			} else {
2536
				$dbw->delete( 'protected_titles',
2537
					[
2538
						'pt_namespace' => $this->mTitle->getNamespace(),
2539
						'pt_title' => $this->mTitle->getDBkey()
2540
					], __METHOD__
2541
				);
2542
			}
2543
		}
2544
2545
		$this->mTitle->flushRestrictions();
2546
		InfoAction::invalidateCache( $this->mTitle );
2547
2548
		if ( $logAction == 'unprotect' ) {
2549
			$params = [];
2550
		} else {
2551
			$protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2552
			$params = [
2553
				'4::description' => $protectDescriptionLog, // parameter for IRC
2554
				'5:bool:cascade' => $cascade,
2555
				'details' => $logParamsDetails, // parameter for localize and api
2556
			];
2557
		}
2558
2559
		// Update the protection log
2560
		$logEntry = new ManualLogEntry( 'protect', $logAction );
2561
		$logEntry->setTarget( $this->mTitle );
2562
		$logEntry->setComment( $reason );
2563
		$logEntry->setPerformer( $user );
2564
		$logEntry->setParameters( $params );
2565
		if ( !is_null( $nullRevision ) ) {
2566
			$logEntry->setAssociatedRevId( $nullRevision->getId() );
2567
		}
2568
		$logEntry->setTags( $tags );
0 ignored issues
show
Bug introduced by
It seems like $tags defined by parameter $tags on line 2337 can also be of type null; however, ManualLogEntry::setTags() does only seem to accept string|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2569
		if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2570
			$logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2571
		}
2572
		$logId = $logEntry->insert();
2573
		$logEntry->publish( $logId );
2574
2575
		return Status::newGood( $logId );
2576
	}
2577
2578
	/**
2579
	 * Insert a new null revision for this page.
2580
	 *
2581
	 * @param string $revCommentMsg Comment message key for the revision
2582
	 * @param array $limit Set of restriction keys
2583
	 * @param array $expiry Per restriction type expiration
2584
	 * @param int $cascade Set to false if cascading protection isn't allowed.
2585
	 * @param string $reason
2586
	 * @param User|null $user
2587
	 * @return Revision|null Null on error
2588
	 */
2589
	public function insertProtectNullRevision( $revCommentMsg, array $limit,
2590
		array $expiry, $cascade, $reason, $user = null
2591
	) {
2592
		global $wgContLang;
2593
		$dbw = wfGetDB( DB_MASTER );
2594
2595
		// Prepare a null revision to be added to the history
2596
		$editComment = $wgContLang->ucfirst(
2597
			wfMessage(
2598
				$revCommentMsg,
2599
				$this->mTitle->getPrefixedText()
2600
			)->inContentLanguage()->text()
2601
		);
2602
		if ( $reason ) {
2603
			$editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2604
		}
2605
		$protectDescription = $this->protectDescription( $limit, $expiry );
2606
		if ( $protectDescription ) {
2607
			$editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2608
			$editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2609
				->inContentLanguage()->text();
2610
		}
2611
		if ( $cascade ) {
2612
			$editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2613
			$editComment .= wfMessage( 'brackets' )->params(
2614
				wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2615
			)->inContentLanguage()->text();
2616
		}
2617
2618
		$nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2619
		if ( $nullRev ) {
2620
			$nullRev->insertOn( $dbw );
2621
2622
			// Update page record and touch page
2623
			$oldLatest = $nullRev->getParentId();
2624
			$this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2625
		}
2626
2627
		return $nullRev;
2628
	}
2629
2630
	/**
2631
	 * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid
2632
	 * @return string
2633
	 */
2634
	protected function formatExpiry( $expiry ) {
2635
		global $wgContLang;
2636
2637
		if ( $expiry != 'infinity' ) {
2638
			return wfMessage(
2639
				'protect-expiring',
2640
				$wgContLang->timeanddate( $expiry, false, false ),
2641
				$wgContLang->date( $expiry, false, false ),
2642
				$wgContLang->time( $expiry, false, false )
2643
			)->inContentLanguage()->text();
2644
		} else {
2645
			return wfMessage( 'protect-expiry-indefinite' )
2646
				->inContentLanguage()->text();
2647
		}
2648
	}
2649
2650
	/**
2651
	 * Builds the description to serve as comment for the edit.
2652
	 *
2653
	 * @param array $limit Set of restriction keys
2654
	 * @param array $expiry Per restriction type expiration
2655
	 * @return string
2656
	 */
2657
	public function protectDescription( array $limit, array $expiry ) {
2658
		$protectDescription = '';
2659
2660
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2661
			# $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
2662
			# All possible message keys are listed here for easier grepping:
2663
			# * restriction-create
2664
			# * restriction-edit
2665
			# * restriction-move
2666
			# * restriction-upload
2667
			$actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2668
			# $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
2669
			# with '' filtered out. All possible message keys are listed below:
2670
			# * protect-level-autoconfirmed
2671
			# * protect-level-sysop
2672
			$restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2673
				->inContentLanguage()->text();
2674
2675
			$expiryText = $this->formatExpiry( $expiry[$action] );
2676
2677
			if ( $protectDescription !== '' ) {
2678
				$protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2679
			}
2680
			$protectDescription .= wfMessage( 'protect-summary-desc' )
2681
				->params( $actionText, $restrictionsText, $expiryText )
2682
				->inContentLanguage()->text();
2683
		}
2684
2685
		return $protectDescription;
2686
	}
2687
2688
	/**
2689
	 * Builds the description to serve as comment for the log entry.
2690
	 *
2691
	 * Some bots may parse IRC lines, which are generated from log entries which contain plain
2692
	 * protect description text. Keep them in old format to avoid breaking compatibility.
2693
	 * TODO: Fix protection log to store structured description and format it on-the-fly.
2694
	 *
2695
	 * @param array $limit Set of restriction keys
2696
	 * @param array $expiry Per restriction type expiration
2697
	 * @return string
2698
	 */
2699
	public function protectDescriptionLog( array $limit, array $expiry ) {
2700
		global $wgContLang;
2701
2702
		$protectDescriptionLog = '';
2703
2704
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2705
			$expiryText = $this->formatExpiry( $expiry[$action] );
2706
			$protectDescriptionLog .= $wgContLang->getDirMark() .
2707
				"[$action=$restrictions] ($expiryText)";
2708
		}
2709
2710
		return trim( $protectDescriptionLog );
2711
	}
2712
2713
	/**
2714
	 * Take an array of page restrictions and flatten it to a string
2715
	 * suitable for insertion into the page_restrictions field.
2716
	 *
2717
	 * @param string[] $limit
2718
	 *
2719
	 * @throws MWException
2720
	 * @return string
2721
	 */
2722
	protected static function flattenRestrictions( $limit ) {
2723
		if ( !is_array( $limit ) ) {
2724
			throw new MWException( __METHOD__ . ' given non-array restriction set' );
2725
		}
2726
2727
		$bits = [];
2728
		ksort( $limit );
2729
2730
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2731
			$bits[] = "$action=$restrictions";
2732
		}
2733
2734
		return implode( ':', $bits );
2735
	}
2736
2737
	/**
2738
	 * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
2739
	 * backwards compatibility, if you care about error reporting you should use
2740
	 * doDeleteArticleReal() instead.
2741
	 *
2742
	 * Deletes the article with database consistency, writes logs, purges caches
2743
	 *
2744
	 * @param string $reason Delete reason for deletion log
2745
	 * @param bool $suppress Suppress all revisions and log the deletion in
2746
	 *        the suppression log instead of the deletion log
2747
	 * @param int $u1 Unused
2748
	 * @param bool $u2 Unused
2749
	 * @param array|string &$error Array of errors to append to
2750
	 * @param User $user The deleting user
2751
	 * @return bool True if successful
2752
	 */
2753
	public function doDeleteArticle(
2754
		$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2755
	) {
2756
		$status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2757
		return $status->isGood();
2758
	}
2759
2760
	/**
2761
	 * Back-end article deletion
2762
	 * Deletes the article with database consistency, writes logs, purges caches
2763
	 *
2764
	 * @since 1.19
2765
	 *
2766
	 * @param string $reason Delete reason for deletion log
2767
	 * @param bool $suppress Suppress all revisions and log the deletion in
2768
	 *   the suppression log instead of the deletion log
2769
	 * @param int $u1 Unused
2770
	 * @param bool $u2 Unused
2771
	 * @param array|string &$error Array of errors to append to
2772
	 * @param User $user The deleting user
2773
	 * @return Status Status object; if successful, $status->value is the log_id of the
2774
	 *   deletion log entry. If the page couldn't be deleted because it wasn't
2775
	 *   found, $status is a non-fatal 'cannotdelete' error
2776
	 */
2777
	public function doDeleteArticleReal(
2778
		$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2779
	) {
2780
		global $wgUser, $wgContentHandlerUseDB;
2781
2782
		wfDebug( __METHOD__ . "\n" );
2783
2784
		$status = Status::newGood();
2785
2786
		if ( $this->mTitle->getDBkey() === '' ) {
2787
			$status->error( 'cannotdelete',
2788
				wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2789
			return $status;
2790
		}
2791
2792
		$user = is_null( $user ) ? $wgUser : $user;
2793
		if ( !Hooks::run( 'ArticleDelete',
2794
			[ &$this, &$user, &$reason, &$error, &$status, $suppress ]
2795
		) ) {
2796
			if ( $status->isOK() ) {
2797
				// Hook aborted but didn't set a fatal status
2798
				$status->fatal( 'delete-hook-aborted' );
2799
			}
2800
			return $status;
2801
		}
2802
2803
		$dbw = wfGetDB( DB_MASTER );
2804
		$dbw->startAtomic( __METHOD__ );
2805
2806
		$this->loadPageData( self::READ_LATEST );
2807
		$id = $this->getId();
2808
		// T98706: lock the page from various other updates but avoid using
2809
		// WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2810
		// the revisions queries (which also JOIN on user). Only lock the page
2811
		// row and CAS check on page_latest to see if the trx snapshot matches.
2812
		$lockedLatest = $this->lockAndGetLatest();
2813
		if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2814
			$dbw->endAtomic( __METHOD__ );
2815
			// Page not there or trx snapshot is stale
2816
			$status->error( 'cannotdelete',
2817
				wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2818
			return $status;
2819
		}
2820
2821
		// At this point we are now comitted to returning an OK
2822
		// status unless some DB query error or other exception comes up.
2823
		// This way callers don't have to call rollback() if $status is bad
2824
		// unless they actually try to catch exceptions (which is rare).
2825
2826
		// we need to remember the old content so we can use it to generate all deletion updates.
2827
		$content = $this->getContent( Revision::RAW );
2828
2829
		// Bitfields to further suppress the content
2830 View Code Duplication
		if ( $suppress ) {
2831
			$bitfield = 0;
2832
			// This should be 15...
2833
			$bitfield |= Revision::DELETED_TEXT;
2834
			$bitfield |= Revision::DELETED_COMMENT;
2835
			$bitfield |= Revision::DELETED_USER;
2836
			$bitfield |= Revision::DELETED_RESTRICTED;
2837
		} else {
2838
			$bitfield = 'rev_deleted';
2839
		}
2840
2841
		/**
2842
		 * For now, shunt the revision data into the archive table.
2843
		 * Text is *not* removed from the text table; bulk storage
2844
		 * is left intact to avoid breaking block-compression or
2845
		 * immutable storage schemes.
2846
		 *
2847
		 * For backwards compatibility, note that some older archive
2848
		 * table entries will have ar_text and ar_flags fields still.
2849
		 *
2850
		 * In the future, we may keep revisions and mark them with
2851
		 * the rev_deleted field, which is reserved for this purpose.
2852
		 */
2853
2854
		$row = [
2855
			'ar_namespace'  => 'page_namespace',
2856
			'ar_title'      => 'page_title',
2857
			'ar_comment'    => 'rev_comment',
2858
			'ar_user'       => 'rev_user',
2859
			'ar_user_text'  => 'rev_user_text',
2860
			'ar_timestamp'  => 'rev_timestamp',
2861
			'ar_minor_edit' => 'rev_minor_edit',
2862
			'ar_rev_id'     => 'rev_id',
2863
			'ar_parent_id'  => 'rev_parent_id',
2864
			'ar_text_id'    => 'rev_text_id',
2865
			'ar_text'       => '\'\'', // Be explicit to appease
2866
			'ar_flags'      => '\'\'', // MySQL's "strict mode"...
2867
			'ar_len'        => 'rev_len',
2868
			'ar_page_id'    => 'page_id',
2869
			'ar_deleted'    => $bitfield,
2870
			'ar_sha1'       => 'rev_sha1',
2871
		];
2872
2873
		if ( $wgContentHandlerUseDB ) {
2874
			$row['ar_content_model'] = 'rev_content_model';
2875
			$row['ar_content_format'] = 'rev_content_format';
2876
		}
2877
2878
		// Copy all the page revisions into the archive table
2879
		$dbw->insertSelect(
2880
			'archive',
2881
			[ 'page', 'revision' ],
2882
			$row,
2883
			[
2884
				'page_id' => $id,
2885
				'page_id = rev_page'
2886
			],
2887
			__METHOD__
2888
		);
2889
2890
		// Now that it's safely backed up, delete it
2891
		$dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2892
2893
		if ( !$dbw->cascadingDeletes() ) {
2894
			$dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2895
		}
2896
2897
		// Clone the title, so we have the information we need when we log
2898
		$logTitle = clone $this->mTitle;
2899
2900
		// Log the deletion, if the page was suppressed, put it in the suppression log instead
2901
		$logtype = $suppress ? 'suppress' : 'delete';
2902
2903
		$logEntry = new ManualLogEntry( $logtype, 'delete' );
2904
		$logEntry->setPerformer( $user );
2905
		$logEntry->setTarget( $logTitle );
2906
		$logEntry->setComment( $reason );
2907
		$logid = $logEntry->insert();
2908
2909
		$dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
2910
			// Bug 56776: avoid deadlocks (especially from FileDeleteForm)
2911
			$logEntry->publish( $logid );
2912
		} );
2913
2914
		$dbw->endAtomic( __METHOD__ );
2915
2916
		$this->doDeleteUpdates( $id, $content );
2917
2918
		Hooks::run( 'ArticleDeleteComplete',
2919
			[ &$this, &$user, $reason, $id, $content, $logEntry ] );
2920
		$status->value = $logid;
2921
2922
		// Show log excerpt on 404 pages rather than just a link
2923
		$cache = ObjectCache::getMainStashInstance();
2924
		$key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2925
		$cache->set( $key, 1, $cache::TTL_DAY );
2926
2927
		return $status;
2928
	}
2929
2930
	/**
2931
	 * Lock the page row for this title+id and return page_latest (or 0)
2932
	 *
2933
	 * @return integer Returns 0 if no row was found with this title+id
2934
	 * @since 1.27
2935
	 */
2936
	public function lockAndGetLatest() {
2937
		return (int)wfGetDB( DB_MASTER )->selectField(
2938
			'page',
2939
			'page_latest',
2940
			[
2941
				'page_id' => $this->getId(),
2942
				// Typically page_id is enough, but some code might try to do
2943
				// updates assuming the title is the same, so verify that
2944
				'page_namespace' => $this->getTitle()->getNamespace(),
2945
				'page_title' => $this->getTitle()->getDBkey()
2946
			],
2947
			__METHOD__,
2948
			[ 'FOR UPDATE' ]
2949
		);
2950
	}
2951
2952
	/**
2953
	 * Do some database updates after deletion
2954
	 *
2955
	 * @param int $id The page_id value of the page being deleted
2956
	 * @param Content $content Optional page content to be used when determining
2957
	 *   the required updates. This may be needed because $this->getContent()
2958
	 *   may already return null when the page proper was deleted.
2959
	 */
2960
	public function doDeleteUpdates( $id, Content $content = null ) {
2961
		// Update site status
2962
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
2963
2964
		// Delete pagelinks, update secondary indexes, etc
2965
		$updates = $this->getDeletionUpdates( $content );
2966
		foreach ( $updates as $update ) {
2967
			DeferredUpdates::addUpdate( $update );
2968
		}
2969
2970
		// Reparse any pages transcluding this page
2971
		LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
2972
2973
		// Reparse any pages including this image
2974
		if ( $this->mTitle->getNamespace() == NS_FILE ) {
2975
			LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
2976
		}
2977
2978
		// Clear caches
2979
		WikiPage::onArticleDelete( $this->mTitle );
2980
2981
		// Reset this object and the Title object
2982
		$this->loadFromRow( false, self::READ_LATEST );
2983
2984
		// Search engine
2985
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
2986
	}
2987
2988
	/**
2989
	 * Roll back the most recent consecutive set of edits to a page
2990
	 * from the same user; fails if there are no eligible edits to
2991
	 * roll back to, e.g. user is the sole contributor. This function
2992
	 * performs permissions checks on $user, then calls commitRollback()
2993
	 * to do the dirty work
2994
	 *
2995
	 * @todo Separate the business/permission stuff out from backend code
2996
	 * @todo Remove $token parameter. Already verified by RollbackAction and ApiRollback.
2997
	 *
2998
	 * @param string $fromP Name of the user whose edits to rollback.
2999
	 * @param string $summary Custom summary. Set to default summary if empty.
3000
	 * @param string $token Rollback token.
3001
	 * @param bool $bot If true, mark all reverted edits as bot.
3002
	 *
3003
	 * @param array $resultDetails Array contains result-specific array of additional values
3004
	 *    'alreadyrolled' : 'current' (rev)
3005
	 *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
3006
	 *
3007
	 * @param User $user The user performing the rollback
3008
	 * @param array|null $tags Change tags to apply to the rollback
3009
	 * Callers are responsible for permission checks
3010
	 * (with ChangeTags::canAddTagsAccompanyingChange)
3011
	 *
3012
	 * @return array Array of errors, each error formatted as
3013
	 *   array(messagekey, param1, param2, ...).
3014
	 * On success, the array is empty.  This array can also be passed to
3015
	 * OutputPage::showPermissionsErrorPage().
3016
	 */
3017
	public function doRollback(
3018
		$fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3019
	) {
3020
		$resultDetails = null;
3021
3022
		// Check permissions
3023
		$editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3024
		$rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3025
		$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3026
3027
		if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3028
			$errors[] = [ 'sessionfailure' ];
3029
		}
3030
3031
		if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3032
			$errors[] = [ 'actionthrottledtext' ];
3033
		}
3034
3035
		// If there were errors, bail out now
3036
		if ( !empty( $errors ) ) {
3037
			return $errors;
3038
		}
3039
3040
		return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3041
	}
3042
3043
	/**
3044
	 * Backend implementation of doRollback(), please refer there for parameter
3045
	 * and return value documentation
3046
	 *
3047
	 * NOTE: This function does NOT check ANY permissions, it just commits the
3048
	 * rollback to the DB. Therefore, you should only call this function direct-
3049
	 * ly if you want to use custom permissions checks. If you don't, use
3050
	 * doRollback() instead.
3051
	 * @param string $fromP Name of the user whose edits to rollback.
3052
	 * @param string $summary Custom summary. Set to default summary if empty.
3053
	 * @param bool $bot If true, mark all reverted edits as bot.
3054
	 *
3055
	 * @param array $resultDetails Contains result-specific array of additional values
3056
	 * @param User $guser The user performing the rollback
3057
	 * @param array|null $tags Change tags to apply to the rollback
3058
	 * Callers are responsible for permission checks
3059
	 * (with ChangeTags::canAddTagsAccompanyingChange)
3060
	 *
3061
	 * @return array
3062
	 */
3063
	public function commitRollback( $fromP, $summary, $bot,
3064
		&$resultDetails, User $guser, $tags = null
3065
	) {
3066
		global $wgUseRCPatrol, $wgContLang;
3067
3068
		$dbw = wfGetDB( DB_MASTER );
3069
3070
		if ( wfReadOnly() ) {
3071
			return [ [ 'readonlytext' ] ];
3072
		}
3073
3074
		// Get the last editor
3075
		$current = $this->getRevision();
3076
		if ( is_null( $current ) ) {
3077
			// Something wrong... no page?
3078
			return [ [ 'notanarticle' ] ];
3079
		}
3080
3081
		$from = str_replace( '_', ' ', $fromP );
3082
		// User name given should match up with the top revision.
3083
		// If the user was deleted then $from should be empty.
3084 View Code Duplication
		if ( $from != $current->getUserText() ) {
3085
			$resultDetails = [ 'current' => $current ];
3086
			return [ [ 'alreadyrolled',
3087
				htmlspecialchars( $this->mTitle->getPrefixedText() ),
3088
				htmlspecialchars( $fromP ),
3089
				htmlspecialchars( $current->getUserText() )
3090
			] ];
3091
		}
3092
3093
		// Get the last edit not by this person...
3094
		// Note: these may not be public values
3095
		$user = intval( $current->getUser( Revision::RAW ) );
3096
		$user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
3097
		$s = $dbw->selectRow( 'revision',
3098
			[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3099
			[ 'rev_page' => $current->getPage(),
3100
				"rev_user != {$user} OR rev_user_text != {$user_text}"
3101
			], __METHOD__,
3102
			[ 'USE INDEX' => 'page_timestamp',
3103
				'ORDER BY' => 'rev_timestamp DESC' ]
3104
			);
3105
		if ( $s === false ) {
3106
			// No one else ever edited this page
3107
			return [ [ 'cantrollback' ] ];
3108
		} elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3109
			|| $s->rev_deleted & Revision::DELETED_USER
3110
		) {
3111
			// Only admins can see this text
3112
			return [ [ 'notvisiblerev' ] ];
3113
		}
3114
3115
		// Generate the edit summary if necessary
3116
		$target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3117
		if ( empty( $summary ) ) {
3118
			if ( $from == '' ) { // no public user name
3119
				$summary = wfMessage( 'revertpage-nouser' );
3120
			} else {
3121
				$summary = wfMessage( 'revertpage' );
3122
			}
3123
		}
3124
3125
		// Allow the custom summary to use the same args as the default message
3126
		$args = [
3127
			$target->getUserText(), $from, $s->rev_id,
3128
			$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3129
			$current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3130
		];
3131
		if ( $summary instanceof Message ) {
3132
			$summary = $summary->params( $args )->inContentLanguage()->text();
3133
		} else {
3134
			$summary = wfMsgReplaceArgs( $summary, $args );
3135
		}
3136
3137
		// Trim spaces on user supplied text
3138
		$summary = trim( $summary );
3139
3140
		// Truncate for whole multibyte characters.
3141
		$summary = $wgContLang->truncate( $summary, 255 );
3142
3143
		// Save
3144
		$flags = EDIT_UPDATE;
3145
3146
		if ( $guser->isAllowed( 'minoredit' ) ) {
3147
			$flags |= EDIT_MINOR;
3148
		}
3149
3150
		if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3151
			$flags |= EDIT_FORCE_BOT;
3152
		}
3153
3154
		// Actually store the edit
3155
		$status = $this->doEditContent(
3156
			$target->getContent(),
3157
			$summary,
3158
			$flags,
3159
			$target->getId(),
3160
			$guser,
3161
			null,
3162
			$tags
3163
		);
3164
3165
		// Set patrolling and bot flag on the edits, which gets rollbacked.
3166
		// This is done even on edit failure to have patrolling in that case (bug 62157).
3167
		$set = [];
3168
		if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3169
			// Mark all reverted edits as bot
3170
			$set['rc_bot'] = 1;
3171
		}
3172
3173
		if ( $wgUseRCPatrol ) {
3174
			// Mark all reverted edits as patrolled
3175
			$set['rc_patrolled'] = 1;
3176
		}
3177
3178
		if ( count( $set ) ) {
3179
			$dbw->update( 'recentchanges', $set,
3180
				[ /* WHERE */
3181
					'rc_cur_id' => $current->getPage(),
3182
					'rc_user_text' => $current->getUserText(),
3183
					'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3184
				],
3185
				__METHOD__
3186
			);
3187
		}
3188
3189
		if ( !$status->isOK() ) {
3190
			return $status->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
3191
		}
3192
3193
		// raise error, when the edit is an edit without a new version
3194
		$statusRev = isset( $status->value['revision'] )
3195
			? $status->value['revision']
3196
			: null;
3197 View Code Duplication
		if ( !( $statusRev instanceof Revision ) ) {
3198
			$resultDetails = [ 'current' => $current ];
3199
			return [ [ 'alreadyrolled',
3200
					htmlspecialchars( $this->mTitle->getPrefixedText() ),
3201
					htmlspecialchars( $fromP ),
3202
					htmlspecialchars( $current->getUserText() )
3203
			] ];
3204
		}
3205
3206
		$revId = $statusRev->getId();
3207
3208
		Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3209
3210
		$resultDetails = [
3211
			'summary' => $summary,
3212
			'current' => $current,
3213
			'target' => $target,
3214
			'newid' => $revId
3215
		];
3216
3217
		return [];
3218
	}
3219
3220
	/**
3221
	 * The onArticle*() functions are supposed to be a kind of hooks
3222
	 * which should be called whenever any of the specified actions
3223
	 * are done.
3224
	 *
3225
	 * This is a good place to put code to clear caches, for instance.
3226
	 *
3227
	 * This is called on page move and undelete, as well as edit
3228
	 *
3229
	 * @param Title $title
3230
	 */
3231
	public static function onArticleCreate( Title $title ) {
3232
		// Update existence markers on article/talk tabs...
3233
		$other = $title->getOtherPage();
3234
3235
		$other->purgeSquid();
3236
3237
		$title->touchLinks();
3238
		$title->purgeSquid();
3239
		$title->deleteTitleProtection();
3240
	}
3241
3242
	/**
3243
	 * Clears caches when article is deleted
3244
	 *
3245
	 * @param Title $title
3246
	 */
3247
	public static function onArticleDelete( Title $title ) {
3248
		global $wgContLang;
3249
3250
		// Update existence markers on article/talk tabs...
3251
		$other = $title->getOtherPage();
3252
3253
		$other->purgeSquid();
3254
3255
		$title->touchLinks();
3256
		$title->purgeSquid();
3257
3258
		// File cache
3259
		HTMLFileCache::clearFileCache( $title );
3260
		InfoAction::invalidateCache( $title );
3261
3262
		// Messages
3263
		if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3264
			MessageCache::singleton()->replace( $title->getDBkey(), false );
3265
3266
			if ( $wgContLang->hasVariants() ) {
3267
				$wgContLang->updateConversionTable( $title );
3268
			}
3269
		}
3270
3271
		// Images
3272
		if ( $title->getNamespace() == NS_FILE ) {
3273
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3274
		}
3275
3276
		// User talk pages
3277
		if ( $title->getNamespace() == NS_USER_TALK ) {
3278
			$user = User::newFromName( $title->getText(), false );
3279
			if ( $user ) {
3280
				$user->setNewtalk( false );
3281
			}
3282
		}
3283
3284
		// Image redirects
3285
		RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3286
	}
3287
3288
	/**
3289
	 * Purge caches on page update etc
3290
	 *
3291
	 * @param Title $title
3292
	 * @param Revision|null $revision Revision that was just saved, may be null
3293
	 */
3294
	public static function onArticleEdit( Title $title, Revision $revision = null ) {
3295
		// Invalidate caches of articles which include this page
3296
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3297
3298
		// Invalidate the caches of all pages which redirect here
3299
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3300
3301
		// Purge CDN for this page only
3302
		$title->purgeSquid();
3303
		// Clear file cache for this page only
3304
		HTMLFileCache::clearFileCache( $title );
3305
3306
		$revid = $revision ? $revision->getId() : null;
3307
		DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
3308
			InfoAction::invalidateCache( $title, $revid );
3309
		} );
3310
	}
3311
3312
	/**#@-*/
3313
3314
	/**
3315
	 * Returns a list of categories this page is a member of.
3316
	 * Results will include hidden categories
3317
	 *
3318
	 * @return TitleArray
3319
	 */
3320
	public function getCategories() {
3321
		$id = $this->getId();
3322
		if ( $id == 0 ) {
3323
			return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3324
		}
3325
3326
		$dbr = wfGetDB( DB_SLAVE );
3327
		$res = $dbr->select( 'categorylinks',
3328
			[ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3329
			// Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
3330
			// as not being aliases, and NS_CATEGORY is numeric
3331
			[ 'cl_from' => $id ],
3332
			__METHOD__ );
3333
3334
		return TitleArray::newFromResult( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('categoryli...m' => $id), __METHOD__) on line 3327 can also be of type boolean; however, TitleArray::newFromResult() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
3335
	}
3336
3337
	/**
3338
	 * Returns a list of hidden categories this page is a member of.
3339
	 * Uses the page_props and categorylinks tables.
3340
	 *
3341
	 * @return array Array of Title objects
3342
	 */
3343
	public function getHiddenCategories() {
3344
		$result = [];
3345
		$id = $this->getId();
3346
3347
		if ( $id == 0 ) {
3348
			return [];
3349
		}
3350
3351
		$dbr = wfGetDB( DB_SLAVE );
3352
		$res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3353
			[ 'cl_to' ],
3354
			[ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3355
				'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3356
			__METHOD__ );
3357
3358
		if ( $res !== false ) {
3359
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3360
				$result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3361
			}
3362
		}
3363
3364
		return $result;
3365
	}
3366
3367
	/**
3368
	 * Return an applicable autosummary if one exists for the given edit.
3369
	 * @param string|null $oldtext The previous text of the page.
3370
	 * @param string|null $newtext The submitted text of the page.
3371
	 * @param int $flags Bitmask: a bitmask of flags submitted for the edit.
3372
	 * @return string An appropriate autosummary, or an empty string.
3373
	 *
3374
	 * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
3375
	 */
3376
	public static function getAutosummary( $oldtext, $newtext, $flags ) {
3377
		// NOTE: stub for backwards-compatibility. assumes the given text is
3378
		// wikitext. will break horribly if it isn't.
3379
3380
		ContentHandler::deprecated( __METHOD__, '1.21' );
3381
3382
		$handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
3383
		$oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3384
		$newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3385
3386
		return $handler->getAutosummary( $oldContent, $newContent, $flags );
3387
	}
3388
3389
	/**
3390
	 * Auto-generates a deletion reason
3391
	 *
3392
	 * @param bool &$hasHistory Whether the page has a history
3393
	 * @return string|bool String containing deletion reason or empty string, or boolean false
3394
	 *    if no revision occurred
3395
	 */
3396
	public function getAutoDeleteReason( &$hasHistory ) {
3397
		return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3398
	}
3399
3400
	/**
3401
	 * Update all the appropriate counts in the category table, given that
3402
	 * we've added the categories $added and deleted the categories $deleted.
3403
	 *
3404
	 * @param array $added The names of categories that were added
3405
	 * @param array $deleted The names of categories that were deleted
3406
	 */
3407
	public function updateCategoryCounts( array $added, array $deleted ) {
3408
		$that = $this;
3409
		$method = __METHOD__;
3410
		$dbw = wfGetDB( DB_MASTER );
3411
3412
		// Do this at the end of the commit to reduce lock wait timeouts
3413
		$dbw->onTransactionPreCommitOrIdle(
3414
			function () use ( $dbw, $that, $method, $added, $deleted ) {
3415
				$ns = $that->getTitle()->getNamespace();
3416
3417
				$addFields = [ 'cat_pages = cat_pages + 1' ];
3418
				$removeFields = [ 'cat_pages = cat_pages - 1' ];
3419
				if ( $ns == NS_CATEGORY ) {
3420
					$addFields[] = 'cat_subcats = cat_subcats + 1';
3421
					$removeFields[] = 'cat_subcats = cat_subcats - 1';
3422
				} elseif ( $ns == NS_FILE ) {
3423
					$addFields[] = 'cat_files = cat_files + 1';
3424
					$removeFields[] = 'cat_files = cat_files - 1';
3425
				}
3426
3427
				if ( count( $added ) ) {
3428
					$existingAdded = $dbw->selectFieldValues(
3429
						'category',
3430
						'cat_title',
3431
						[ 'cat_title' => $added ],
3432
						__METHOD__
3433
					);
3434
3435
					// For category rows that already exist, do a plain
3436
					// UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3437
					// to avoid creating gaps in the cat_id sequence.
3438
					if ( count( $existingAdded ) ) {
3439
						$dbw->update(
3440
							'category',
3441
							$addFields,
3442
							[ 'cat_title' => $existingAdded ],
3443
							__METHOD__
3444
						);
3445
					}
3446
3447
					$missingAdded = array_diff( $added, $existingAdded );
3448
					if ( count( $missingAdded ) ) {
3449
						$insertRows = [];
3450
						foreach ( $missingAdded as $cat ) {
3451
							$insertRows[] = [
3452
								'cat_title'   => $cat,
3453
								'cat_pages'   => 1,
3454
								'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3455
								'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
3456
							];
3457
						}
3458
						$dbw->upsert(
3459
							'category',
3460
							$insertRows,
3461
							[ 'cat_title' ],
3462
							$addFields,
3463
							$method
3464
						);
3465
					}
3466
				}
3467
3468
				if ( count( $deleted ) ) {
3469
					$dbw->update(
3470
						'category',
3471
						$removeFields,
3472
						[ 'cat_title' => $deleted ],
3473
						$method
3474
					);
3475
				}
3476
3477
				foreach ( $added as $catName ) {
3478
					$cat = Category::newFromName( $catName );
3479
					Hooks::run( 'CategoryAfterPageAdded', [ $cat, $that ] );
3480
				}
3481
3482
				foreach ( $deleted as $catName ) {
3483
					$cat = Category::newFromName( $catName );
3484
					Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $that ] );
3485
				}
3486
			}
3487
		);
3488
	}
3489
3490
	/**
3491
	 * Opportunistically enqueue link update jobs given fresh parser output if useful
3492
	 *
3493
	 * @param ParserOutput $parserOutput Current version page output
3494
	 * @since 1.25
3495
	 */
3496
	public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3497
		if ( wfReadOnly() ) {
3498
			return;
3499
		}
3500
3501
		if ( !Hooks::run( 'OpportunisticLinksUpdate',
3502
			[ $this, $this->mTitle, $parserOutput ]
3503
		) ) {
3504
			return;
3505
		}
3506
3507
		$config = RequestContext::getMain()->getConfig();
3508
3509
		$params = [
3510
			'isOpportunistic' => true,
3511
			'rootJobTimestamp' => $parserOutput->getCacheTime()
3512
		];
3513
3514
		if ( $this->mTitle->areRestrictionsCascading() ) {
3515
			// If the page is cascade protecting, the links should really be up-to-date
3516
			JobQueueGroup::singleton()->lazyPush(
3517
				RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3518
			);
3519
		} elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3520
			// Assume the output contains "dynamic" time/random based magic words.
3521
			// Only update pages that expired due to dynamic content and NOT due to edits
3522
			// to referenced templates/files. When the cache expires due to dynamic content,
3523
			// page_touched is unchanged. We want to avoid triggering redundant jobs due to
3524
			// views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3525
			// template/file edit already triggered recursive RefreshLinksJob jobs.
3526
			if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3527
				// If a page is uncacheable, do not keep spamming a job for it.
3528
				// Although it would be de-duplicated, it would still waste I/O.
3529
				$cache = ObjectCache::getLocalClusterInstance();
3530
				$key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3531
				$ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3532
				if ( $cache->add( $key, time(), $ttl ) ) {
3533
					JobQueueGroup::singleton()->lazyPush(
3534
						RefreshLinksJob::newDynamic( $this->mTitle, $params )
3535
					);
3536
				}
3537
			}
3538
		}
3539
	}
3540
3541
	/**
3542
	 * Returns a list of updates to be performed when this page is deleted. The
3543
	 * updates should remove any information about this page from secondary data
3544
	 * stores such as links tables.
3545
	 *
3546
	 * @param Content|null $content Optional Content object for determining the
3547
	 *   necessary updates.
3548
	 * @return DataUpdate[]
3549
	 */
3550
	public function getDeletionUpdates( Content $content = null ) {
3551
		if ( !$content ) {
3552
			// load content object, which may be used to determine the necessary updates.
3553
			// XXX: the content may not be needed to determine the updates.
3554
			$content = $this->getContent( Revision::RAW );
3555
		}
3556
3557
		if ( !$content ) {
3558
			$updates = [];
3559
		} else {
3560
			$updates = $content->getDeletionUpdates( $this );
3561
		}
3562
3563
		Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3564
		return $updates;
3565
	}
3566
}
3567