Completed
Branch master (9ca75b)
by
unknown
26:06
created

WikiPage::doDeleteArticleReal()   D

Complexity

Conditions 13
Paths 87

Size

Total Lines 170
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 101
nc 87
nop 7
dl 0
loc 170
rs 4.9922
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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
use \MediaWiki\Logger\LoggerFactory;
24
use \MediaWiki\MediaWikiServices;
25
26
/**
27
 * Class representing a MediaWiki article and history.
28
 *
29
 * Some fields are public only for backwards-compatibility. Use accessors.
30
 * In the past, this class was part of Article.php and everything was public.
31
 */
32
class WikiPage implements Page, IDBAccessObject {
33
	// Constants for $mDataLoadedFrom and related
34
35
	/**
36
	 * @var Title
37
	 */
38
	public $mTitle = null;
39
40
	/**@{{
41
	 * @protected
42
	 */
43
	public $mDataLoaded = false;         // !< Boolean
44
	public $mIsRedirect = false;         // !< Boolean
45
	public $mLatest = false;             // !< Integer (false means "not loaded")
46
	/**@}}*/
47
48
	/** @var stdClass Map of cache fields (text, parser output, ect) for a proposed/new edit */
49
	public $mPreparedEdit = false;
50
51
	/**
52
	 * @var int
53
	 */
54
	protected $mId = null;
55
56
	/**
57
	 * @var int One of the READ_* constants
58
	 */
59
	protected $mDataLoadedFrom = self::READ_NONE;
60
61
	/**
62
	 * @var Title
63
	 */
64
	protected $mRedirectTarget = null;
65
66
	/**
67
	 * @var Revision
68
	 */
69
	protected $mLastRevision = null;
70
71
	/**
72
	 * @var string Timestamp of the current revision or empty string if not loaded
73
	 */
74
	protected $mTimestamp = '';
75
76
	/**
77
	 * @var string
78
	 */
79
	protected $mTouched = '19700101000000';
80
81
	/**
82
	 * @var string
83
	 */
84
	protected $mLinksUpdated = '19700101000000';
85
86
	const PURGE_CDN_CACHE = 1; // purge CDN cache for page variant URLs
87
	const PURGE_CLUSTER_PCACHE = 2; // purge parser cache in the local datacenter
88
	const PURGE_GLOBAL_PCACHE = 4; // set page_touched to clear parser cache in all datacenters
89
	const PURGE_ALL = 7;
90
91
	/**
92
	 * Constructor and clear the article
93
	 * @param Title $title Reference to a Title object.
94
	 */
95
	public function __construct( Title $title ) {
96
		$this->mTitle = $title;
97
	}
98
99
	/**
100
	 * Makes sure that the mTitle object is cloned
101
	 * to the newly cloned WikiPage.
102
	 */
103
	public function __clone() {
104
		$this->mTitle = clone $this->mTitle;
105
	}
106
107
	/**
108
	 * Create a WikiPage object of the appropriate class for the given title.
109
	 *
110
	 * @param Title $title
111
	 *
112
	 * @throws MWException
113
	 * @return WikiPage|WikiCategoryPage|WikiFilePage
114
	 */
115
	public static function factory( Title $title ) {
116
		$ns = $title->getNamespace();
117
118
		if ( $ns == NS_MEDIA ) {
119
			throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
120
		} elseif ( $ns < 0 ) {
121
			throw new MWException( "Invalid or virtual namespace $ns given." );
122
		}
123
124
		$page = null;
125
		if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
126
			return $page;
127
		}
128
129
		switch ( $ns ) {
130
			case NS_FILE:
131
				$page = new WikiFilePage( $title );
132
				break;
133
			case NS_CATEGORY:
134
				$page = new WikiCategoryPage( $title );
135
				break;
136
			default:
137
				$page = new WikiPage( $title );
138
		}
139
140
		return $page;
141
	}
142
143
	/**
144
	 * Constructor from a page id
145
	 *
146
	 * @param int $id Article ID to load
147
	 * @param string|int $from One of the following values:
148
	 *        - "fromdb" or WikiPage::READ_NORMAL to select from a replica DB
149
	 *        - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
150
	 *
151
	 * @return WikiPage|null
152
	 */
153
	public static function newFromID( $id, $from = 'fromdb' ) {
154
		// page id's are never 0 or negative, see bug 61166
155
		if ( $id < 1 ) {
156
			return null;
157
		}
158
159
		$from = self::convertSelectType( $from );
160
		$db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
161
		$row = $db->selectRow(
162
			'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
163
		if ( !$row ) {
164
			return null;
165
		}
166
		return self::newFromRow( $row, $from );
0 ignored issues
show
Bug introduced by
It seems like $from defined by self::convertSelectType($from) on line 159 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...
167
	}
168
169
	/**
170
	 * Constructor from a database row
171
	 *
172
	 * @since 1.20
173
	 * @param object $row Database row containing at least fields returned by selectFields().
174
	 * @param string|int $from Source of $data:
175
	 *        - "fromdb" or WikiPage::READ_NORMAL: from a replica DB
176
	 *        - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
177
	 *        - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
178
	 * @return WikiPage
179
	 */
180
	public static function newFromRow( $row, $from = 'fromdb' ) {
181
		$page = self::factory( Title::newFromRow( $row ) );
182
		$page->loadFromRow( $row, $from );
183
		return $page;
184
	}
185
186
	/**
187
	 * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
188
	 *
189
	 * @param object|string|int $type
190
	 * @return mixed
191
	 */
192
	private static function convertSelectType( $type ) {
193
		switch ( $type ) {
194
		case 'fromdb':
195
			return self::READ_NORMAL;
196
		case 'fromdbmaster':
197
			return self::READ_LATEST;
198
		case 'forupdate':
199
			return self::READ_LOCKING;
200
		default:
201
			// It may already be an integer or whatever else
202
			return $type;
203
		}
204
	}
205
206
	/**
207
	 * @todo Move this UI stuff somewhere else
208
	 *
209
	 * @see ContentHandler::getActionOverrides
210
	 */
211
	public function getActionOverrides() {
212
		return $this->getContentHandler()->getActionOverrides();
213
	}
214
215
	/**
216
	 * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
217
	 *
218
	 * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
219
	 *
220
	 * @return ContentHandler
221
	 *
222
	 * @since 1.21
223
	 */
224
	public function getContentHandler() {
225
		return ContentHandler::getForModelID( $this->getContentModel() );
226
	}
227
228
	/**
229
	 * Get the title object of the article
230
	 * @return Title Title object of this page
231
	 */
232
	public function getTitle() {
233
		return $this->mTitle;
234
	}
235
236
	/**
237
	 * Clear the object
238
	 * @return void
239
	 */
240
	public function clear() {
241
		$this->mDataLoaded = false;
242
		$this->mDataLoadedFrom = self::READ_NONE;
243
244
		$this->clearCacheFields();
245
	}
246
247
	/**
248
	 * Clear the object cache fields
249
	 * @return void
250
	 */
251
	protected function clearCacheFields() {
252
		$this->mId = null;
253
		$this->mRedirectTarget = null; // Title object if set
254
		$this->mLastRevision = null; // Latest revision
255
		$this->mTouched = '19700101000000';
256
		$this->mLinksUpdated = '19700101000000';
257
		$this->mTimestamp = '';
258
		$this->mIsRedirect = false;
259
		$this->mLatest = false;
260
		// Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
261
		// the requested rev ID and content against the cached one for equality. For most
262
		// content types, the output should not change during the lifetime of this cache.
263
		// Clearing it can cause extra parses on edit for no reason.
264
	}
265
266
	/**
267
	 * Clear the mPreparedEdit cache field, as may be needed by mutable content types
268
	 * @return void
269
	 * @since 1.23
270
	 */
271
	public function clearPreparedEdit() {
272
		$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...
273
	}
274
275
	/**
276
	 * Return the list of revision fields that should be selected to create
277
	 * a new page.
278
	 *
279
	 * @return array
280
	 */
281
	public static function selectFields() {
282
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
283
284
		$fields = [
285
			'page_id',
286
			'page_namespace',
287
			'page_title',
288
			'page_restrictions',
289
			'page_is_redirect',
290
			'page_is_new',
291
			'page_random',
292
			'page_touched',
293
			'page_links_updated',
294
			'page_latest',
295
			'page_len',
296
		];
297
298
		if ( $wgContentHandlerUseDB ) {
299
			$fields[] = 'page_content_model';
300
		}
301
302
		if ( $wgPageLanguageUseDB ) {
303
			$fields[] = 'page_lang';
304
		}
305
306
		return $fields;
307
	}
308
309
	/**
310
	 * Fetch a page record with the given conditions
311
	 * @param IDatabase $dbr
312
	 * @param array $conditions
313
	 * @param array $options
314
	 * @return object|bool Database result resource, or false on failure
315
	 */
316
	protected function pageData( $dbr, $conditions, $options = [] ) {
317
		$fields = self::selectFields();
318
319
		Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
320
321
		$row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
322
323
		Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
324
325
		return $row;
326
	}
327
328
	/**
329
	 * Fetch a page record matching the Title object's namespace and title
330
	 * using a sanitized title string
331
	 *
332
	 * @param IDatabase $dbr
333
	 * @param Title $title
334
	 * @param array $options
335
	 * @return object|bool Database result resource, or false on failure
336
	 */
337
	public function pageDataFromTitle( $dbr, $title, $options = [] ) {
338
		return $this->pageData( $dbr, [
339
			'page_namespace' => $title->getNamespace(),
340
			'page_title' => $title->getDBkey() ], $options );
341
	}
342
343
	/**
344
	 * Fetch a page record matching the requested ID
345
	 *
346
	 * @param IDatabase $dbr
347
	 * @param int $id
348
	 * @param array $options
349
	 * @return object|bool Database result resource, or false on failure
350
	 */
351
	public function pageDataFromId( $dbr, $id, $options = [] ) {
352
		return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
353
	}
354
355
	/**
356
	 * Load the object from a given source by title
357
	 *
358
	 * @param object|string|int $from One of the following:
359
	 *   - A DB query result object.
360
	 *   - "fromdb" or WikiPage::READ_NORMAL to get from a replica DB.
361
	 *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
362
	 *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
363
	 *     using SELECT FOR UPDATE.
364
	 *
365
	 * @return void
366
	 */
367
	public function loadPageData( $from = 'fromdb' ) {
368
		$from = self::convertSelectType( $from );
369
		if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
370
			// We already have the data from the correct location, no need to load it twice.
371
			return;
372
		}
373
374
		if ( is_int( $from ) ) {
375
			list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
376
			$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
0 ignored issues
show
Bug introduced by
It seems like wfGetDB($index) can be null; however, pageDataFromTitle() 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...
377
378
			if ( !$data
379
				&& $index == DB_REPLICA
380
				&& 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...
381
				&& 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...
382
			) {
383
				$from = self::READ_LATEST;
384
				list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
385
				$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
0 ignored issues
show
Bug introduced by
It seems like wfGetDB($index) can be null; however, pageDataFromTitle() 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...
386
			}
387
		} else {
388
			// No idea from where the caller got this data, assume replica DB.
389
			$data = $from;
390
			$from = self::READ_NORMAL;
391
		}
392
393
		$this->loadFromRow( $data, $from );
0 ignored issues
show
Bug introduced by
It seems like $data defined by $from on line 389 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...
394
	}
395
396
	/**
397
	 * Load the object from a database row
398
	 *
399
	 * @since 1.20
400
	 * @param object|bool $data DB row containing fields returned by selectFields() or false
401
	 * @param string|int $from One of the following:
402
	 *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a replica DB
403
	 *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
404
	 *        - "forupdate"  or WikiPage::READ_LOCKING if the data comes from
405
	 *          the master DB using SELECT FOR UPDATE
406
	 */
407
	public function loadFromRow( $data, $from ) {
408
		$lc = LinkCache::singleton();
409
		$lc->clearLink( $this->mTitle );
410
411
		if ( $data ) {
412
			$lc->addGoodLinkObjFromRow( $this->mTitle, $data );
413
414
			$this->mTitle->loadFromRow( $data );
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 407 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...
415
416
			// Old-fashioned restrictions
417
			$this->mTitle->loadRestrictions( $data->page_restrictions );
418
419
			$this->mId = intval( $data->page_id );
420
			$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...
421
			$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...
422
			$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...
423
			$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...
424
			// Bug 37225: $latest may no longer match the cached latest Revision object.
425
			// Double-check the ID of any cached latest Revision object for consistency.
426
			if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
427
				$this->mLastRevision = null;
428
				$this->mTimestamp = '';
429
			}
430
		} else {
431
			$lc->addBadLinkObj( $this->mTitle );
432
433
			$this->mTitle->loadFromRow( false );
434
435
			$this->clearCacheFields();
436
437
			$this->mId = 0;
438
		}
439
440
		$this->mDataLoaded = true;
441
		$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...
442
	}
443
444
	/**
445
	 * @return int Page ID
446
	 */
447
	public function getId() {
448
		if ( !$this->mDataLoaded ) {
449
			$this->loadPageData();
450
		}
451
		return $this->mId;
452
	}
453
454
	/**
455
	 * @return bool Whether or not the page exists in the database
456
	 */
457
	public function exists() {
458
		if ( !$this->mDataLoaded ) {
459
			$this->loadPageData();
460
		}
461
		return $this->mId > 0;
462
	}
463
464
	/**
465
	 * Check if this page is something we're going to be showing
466
	 * some sort of sensible content for. If we return false, page
467
	 * views (plain action=view) will return an HTTP 404 response,
468
	 * so spiders and robots can know they're following a bad link.
469
	 *
470
	 * @return bool
471
	 */
472
	public function hasViewableContent() {
473
		return $this->mTitle->isKnown();
474
	}
475
476
	/**
477
	 * Tests if the article content represents a redirect
478
	 *
479
	 * @return bool
480
	 */
481
	public function isRedirect() {
482
		if ( !$this->mDataLoaded ) {
483
			$this->loadPageData();
484
		}
485
486
		return (bool)$this->mIsRedirect;
487
	}
488
489
	/**
490
	 * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
491
	 *
492
	 * Will use the revisions actual content model if the page exists,
493
	 * and the page's default if the page doesn't exist yet.
494
	 *
495
	 * @return string
496
	 *
497
	 * @since 1.21
498
	 */
499
	public function getContentModel() {
500
		if ( $this->exists() ) {
501
			$cache = ObjectCache::getMainWANInstance();
502
503
			return $cache->getWithSetCallback(
504
				$cache->makeKey( 'page', 'content-model', $this->getLatest() ),
505
				$cache::TTL_MONTH,
506
				function () {
507
					$rev = $this->getRevision();
508
					if ( $rev ) {
509
						// Look at the revision's actual content model
510
						return $rev->getContentModel();
511
					} else {
512
						$title = $this->mTitle->getPrefixedDBkey();
513
						wfWarn( "Page $title exists but has no (visible) revisions!" );
514
						return $this->mTitle->getContentModel();
515
					}
516
				}
517
			);
518
		}
519
520
		// use the default model for this page
521
		return $this->mTitle->getContentModel();
522
	}
523
524
	/**
525
	 * Loads page_touched and returns a value indicating if it should be used
526
	 * @return bool True if this page exists and is not a redirect
527
	 */
528
	public function checkTouched() {
529
		if ( !$this->mDataLoaded ) {
530
			$this->loadPageData();
531
		}
532
		return ( $this->mId && !$this->mIsRedirect );
533
	}
534
535
	/**
536
	 * Get the page_touched field
537
	 * @return string Containing GMT timestamp
538
	 */
539
	public function getTouched() {
540
		if ( !$this->mDataLoaded ) {
541
			$this->loadPageData();
542
		}
543
		return $this->mTouched;
544
	}
545
546
	/**
547
	 * Get the page_links_updated field
548
	 * @return string|null Containing GMT timestamp
549
	 */
550
	public function getLinksTimestamp() {
551
		if ( !$this->mDataLoaded ) {
552
			$this->loadPageData();
553
		}
554
		return $this->mLinksUpdated;
555
	}
556
557
	/**
558
	 * Get the page_latest field
559
	 * @return int The rev_id of current revision
560
	 */
561
	public function getLatest() {
562
		if ( !$this->mDataLoaded ) {
563
			$this->loadPageData();
564
		}
565
		return (int)$this->mLatest;
566
	}
567
568
	/**
569
	 * Get the Revision object of the oldest revision
570
	 * @return Revision|null
571
	 */
572
	public function getOldestRevision() {
573
574
		// Try using the replica DB first, then try the master
575
		$continue = 2;
576
		$db = wfGetDB( DB_REPLICA );
577
		$revSelectFields = Revision::selectFields();
578
579
		$row = null;
580
		while ( $continue ) {
581
			$row = $db->selectRow(
582
				[ 'page', 'revision' ],
583
				$revSelectFields,
584
				[
585
					'page_namespace' => $this->mTitle->getNamespace(),
586
					'page_title' => $this->mTitle->getDBkey(),
587
					'rev_page = page_id'
588
				],
589
				__METHOD__,
590
				[
591
					'ORDER BY' => 'rev_timestamp ASC'
592
				]
593
			);
594
595
			if ( $row ) {
596
				$continue = 0;
597
			} else {
598
				$db = wfGetDB( DB_MASTER );
599
				$continue--;
600
			}
601
		}
602
603
		return $row ? Revision::newFromRow( $row ) : null;
604
	}
605
606
	/**
607
	 * Loads everything except the text
608
	 * This isn't necessary for all uses, so it's only done if needed.
609
	 */
610
	protected function loadLastEdit() {
611
		if ( $this->mLastRevision !== null ) {
612
			return; // already loaded
613
		}
614
615
		$latest = $this->getLatest();
616
		if ( !$latest ) {
617
			return; // page doesn't exist or is missing page_latest info
618
		}
619
620
		if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
621
			// Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
622
			// includes the latest changes committed. This is true even within REPEATABLE-READ
623
			// transactions, where S1 normally only sees changes committed before the first S1
624
			// SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
625
			// may not find it since a page row UPDATE and revision row INSERT by S2 may have
626
			// happened after the first S1 SELECT.
627
			// http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
628
			$flags = Revision::READ_LOCKING;
629
			$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
630
		} elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
631
			// Bug T93976: if page_latest was loaded from the master, fetch the
632
			// revision from there as well, as it may not exist yet on a replica DB.
633
			// Also, this keeps the queries in the same REPEATABLE-READ snapshot.
634
			$flags = Revision::READ_LATEST;
635
			$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
636
		} else {
637
			$dbr = wfGetDB( DB_REPLICA );
638
			$revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
0 ignored issues
show
Bug introduced by
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 637 can be null; however, Revision::newKnownCurrent() 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...
639
		}
640
641
		if ( $revision ) { // sanity
642
			$this->setLastEdit( $revision );
0 ignored issues
show
Bug introduced by
It seems like $revision can also be of type boolean; however, WikiPage::setLastEdit() does only seem to accept object<Revision>, 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...
643
		}
644
	}
645
646
	/**
647
	 * Set the latest revision
648
	 * @param Revision $revision
649
	 */
650
	protected function setLastEdit( Revision $revision ) {
651
		$this->mLastRevision = $revision;
652
		$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...
653
	}
654
655
	/**
656
	 * Get the latest revision
657
	 * @return Revision|null
658
	 */
659
	public function getRevision() {
660
		$this->loadLastEdit();
661
		if ( $this->mLastRevision ) {
662
			return $this->mLastRevision;
663
		}
664
		return null;
665
	}
666
667
	/**
668
	 * Get the content of the current revision. No side-effects...
669
	 *
670
	 * @param int $audience One of:
671
	 *   Revision::FOR_PUBLIC       to be displayed to all users
672
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
673
	 *   Revision::RAW              get the text regardless of permissions
674
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
675
	 *   to the $audience parameter
676
	 * @return Content|null The content of the current revision
677
	 *
678
	 * @since 1.21
679
	 */
680 View Code Duplication
	public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
681
		$this->loadLastEdit();
682
		if ( $this->mLastRevision ) {
683
			return $this->mLastRevision->getContent( $audience, $user );
684
		}
685
		return null;
686
	}
687
688
	/**
689
	 * Get the text of the current revision. No side-effects...
690
	 *
691
	 * @param int $audience One of:
692
	 *   Revision::FOR_PUBLIC       to be displayed to all users
693
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
694
	 *   Revision::RAW              get the text regardless of permissions
695
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
696
	 *   to the $audience parameter
697
	 * @return string|bool The text of the current revision
698
	 * @deprecated since 1.21, getContent() should be used instead.
699
	 */
700
	public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
701
		wfDeprecated( __METHOD__, '1.21' );
702
703
		$this->loadLastEdit();
704
		if ( $this->mLastRevision ) {
705
			return $this->mLastRevision->getText( $audience, $user );
706
		}
707
		return false;
708
	}
709
710
	/**
711
	 * @return string MW timestamp of last article revision
712
	 */
713
	public function getTimestamp() {
714
		// Check if the field has been filled by WikiPage::setTimestamp()
715
		if ( !$this->mTimestamp ) {
716
			$this->loadLastEdit();
717
		}
718
719
		return wfTimestamp( TS_MW, $this->mTimestamp );
720
	}
721
722
	/**
723
	 * Set the page timestamp (use only to avoid DB queries)
724
	 * @param string $ts MW timestamp of last article revision
725
	 * @return void
726
	 */
727
	public function setTimestamp( $ts ) {
728
		$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...
729
	}
730
731
	/**
732
	 * @param int $audience One of:
733
	 *   Revision::FOR_PUBLIC       to be displayed to all users
734
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
735
	 *   Revision::RAW              get the text regardless of permissions
736
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
737
	 *   to the $audience parameter
738
	 * @return int User ID for the user that made the last article revision
739
	 */
740 View Code Duplication
	public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
741
		$this->loadLastEdit();
742
		if ( $this->mLastRevision ) {
743
			return $this->mLastRevision->getUser( $audience, $user );
744
		} else {
745
			return -1;
746
		}
747
	}
748
749
	/**
750
	 * Get the User object of the user who created the page
751
	 * @param int $audience One of:
752
	 *   Revision::FOR_PUBLIC       to be displayed to all users
753
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
754
	 *   Revision::RAW              get the text regardless of permissions
755
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
756
	 *   to the $audience parameter
757
	 * @return User|null
758
	 */
759
	public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
760
		$revision = $this->getOldestRevision();
761
		if ( $revision ) {
762
			$userName = $revision->getUserText( $audience, $user );
763
			return User::newFromName( $userName, false );
0 ignored issues
show
Bug introduced by
It seems like $userName defined by $revision->getUserText($audience, $user) on line 762 can also be of type boolean; however, User::newFromName() does only seem to accept string, 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...
764
		} else {
765
			return null;
766
		}
767
	}
768
769
	/**
770
	 * @param int $audience One of:
771
	 *   Revision::FOR_PUBLIC       to be displayed to all users
772
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
773
	 *   Revision::RAW              get the text regardless of permissions
774
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
775
	 *   to the $audience parameter
776
	 * @return string Username of the user that made the last article revision
777
	 */
778 View Code Duplication
	public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
779
		$this->loadLastEdit();
780
		if ( $this->mLastRevision ) {
781
			return $this->mLastRevision->getUserText( $audience, $user );
782
		} else {
783
			return '';
784
		}
785
	}
786
787
	/**
788
	 * @param int $audience One of:
789
	 *   Revision::FOR_PUBLIC       to be displayed to all users
790
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
791
	 *   Revision::RAW              get the text regardless of permissions
792
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
793
	 *   to the $audience parameter
794
	 * @return string Comment stored for the last article revision
795
	 */
796 View Code Duplication
	public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
797
		$this->loadLastEdit();
798
		if ( $this->mLastRevision ) {
799
			return $this->mLastRevision->getComment( $audience, $user );
800
		} else {
801
			return '';
802
		}
803
	}
804
805
	/**
806
	 * Returns true if last revision was marked as "minor edit"
807
	 *
808
	 * @return bool Minor edit indicator for the last article revision.
809
	 */
810
	public function getMinorEdit() {
811
		$this->loadLastEdit();
812
		if ( $this->mLastRevision ) {
813
			return $this->mLastRevision->isMinor();
814
		} else {
815
			return false;
816
		}
817
	}
818
819
	/**
820
	 * Determine whether a page would be suitable for being counted as an
821
	 * article in the site_stats table based on the title & its content
822
	 *
823
	 * @param object|bool $editInfo (false): object returned by prepareTextForEdit(),
824
	 *   if false, the current database state will be used
825
	 * @return bool
826
	 */
827
	public function isCountable( $editInfo = false ) {
828
		global $wgArticleCountMethod;
829
830
		if ( !$this->mTitle->isContentPage() ) {
831
			return false;
832
		}
833
834
		if ( $editInfo ) {
835
			$content = $editInfo->pstContent;
836
		} else {
837
			$content = $this->getContent();
838
		}
839
840
		if ( !$content || $content->isRedirect() ) {
841
			return false;
842
		}
843
844
		$hasLinks = null;
845
846
		if ( $wgArticleCountMethod === 'link' ) {
847
			// nasty special case to avoid re-parsing to detect links
848
849
			if ( $editInfo ) {
850
				// ParserOutput::getLinks() is a 2D array of page links, so
851
				// to be really correct we would need to recurse in the array
852
				// but the main array should only have items in it if there are
853
				// links.
854
				$hasLinks = (bool)count( $editInfo->output->getLinks() );
855
			} else {
856
				$hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
857
					[ 'pl_from' => $this->getId() ], __METHOD__ );
858
			}
859
		}
860
861
		return $content->isCountable( $hasLinks );
862
	}
863
864
	/**
865
	 * If this page is a redirect, get its target
866
	 *
867
	 * The target will be fetched from the redirect table if possible.
868
	 * If this page doesn't have an entry there, call insertRedirect()
869
	 * @return Title|null Title object, or null if this page is not a redirect
870
	 */
871
	public function getRedirectTarget() {
872
		if ( !$this->mTitle->isRedirect() ) {
873
			return null;
874
		}
875
876
		if ( $this->mRedirectTarget !== null ) {
877
			return $this->mRedirectTarget;
878
		}
879
880
		// Query the redirect table
881
		$dbr = wfGetDB( DB_REPLICA );
882
		$row = $dbr->selectRow( 'redirect',
883
			[ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
884
			[ 'rd_from' => $this->getId() ],
885
			__METHOD__
886
		);
887
888
		// rd_fragment and rd_interwiki were added later, populate them if empty
889
		if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
890
			$this->mRedirectTarget = Title::makeTitle(
891
				$row->rd_namespace, $row->rd_title,
892
				$row->rd_fragment, $row->rd_interwiki
893
			);
894
			return $this->mRedirectTarget;
895
		}
896
897
		// This page doesn't have an entry in the redirect table
898
		$this->mRedirectTarget = $this->insertRedirect();
899
		return $this->mRedirectTarget;
900
	}
901
902
	/**
903
	 * Insert an entry for this page into the redirect table if the content is a redirect
904
	 *
905
	 * The database update will be deferred via DeferredUpdates
906
	 *
907
	 * Don't call this function directly unless you know what you're doing.
908
	 * @return Title|null Title object or null if not a redirect
909
	 */
910
	public function insertRedirect() {
911
		$content = $this->getContent();
912
		$retval = $content ? $content->getUltimateRedirectTarget() : null;
913
		if ( !$retval ) {
914
			return null;
915
		}
916
917
		// Update the DB post-send if the page has not cached since now
918
		$that = $this;
919
		$latest = $this->getLatest();
920
		DeferredUpdates::addCallableUpdate(
921
			function () use ( $that, $retval, $latest ) {
922
				$that->insertRedirectEntry( $retval, $latest );
923
			},
924
			DeferredUpdates::POSTSEND,
925
			wfGetDB( DB_MASTER )
926
		);
927
928
		return $retval;
929
	}
930
931
	/**
932
	 * Insert or update the redirect table entry for this page to indicate it redirects to $rt
933
	 * @param Title $rt Redirect target
934
	 * @param int|null $oldLatest Prior page_latest for check and set
935
	 */
936
	public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
937
		$dbw = wfGetDB( DB_MASTER );
938
		$dbw->startAtomic( __METHOD__ );
939
940
		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...
941
			$dbw->replace( 'redirect',
942
				[ 'rd_from' ],
943
				[
944
					'rd_from' => $this->getId(),
945
					'rd_namespace' => $rt->getNamespace(),
946
					'rd_title' => $rt->getDBkey(),
947
					'rd_fragment' => $rt->getFragment(),
948
					'rd_interwiki' => $rt->getInterwiki(),
949
				],
950
				__METHOD__
951
			);
952
		}
953
954
		$dbw->endAtomic( __METHOD__ );
955
	}
956
957
	/**
958
	 * Get the Title object or URL this page redirects to
959
	 *
960
	 * @return bool|Title|string False, Title of in-wiki target, or string with URL
961
	 */
962
	public function followRedirect() {
963
		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...
964
	}
965
966
	/**
967
	 * Get the Title object or URL to use for a redirect. We use Title
968
	 * objects for same-wiki, non-special redirects and URLs for everything
969
	 * else.
970
	 * @param Title $rt Redirect target
971
	 * @return bool|Title|string False, Title object of local target, or string with URL
972
	 */
973
	public function getRedirectURL( $rt ) {
974
		if ( !$rt ) {
975
			return false;
976
		}
977
978
		if ( $rt->isExternal() ) {
979
			if ( $rt->isLocal() ) {
980
				// Offsite wikis need an HTTP redirect.
981
				// This can be hard to reverse and may produce loops,
982
				// so they may be disabled in the site configuration.
983
				$source = $this->mTitle->getFullURL( 'redirect=no' );
984
				return $rt->getFullURL( [ 'rdfrom' => $source ] );
985
			} else {
986
				// External pages without "local" bit set are not valid
987
				// redirect targets
988
				return false;
989
			}
990
		}
991
992
		if ( $rt->isSpecialPage() ) {
993
			// Gotta handle redirects to special pages differently:
994
			// Fill the HTTP response "Location" header and ignore the rest of the page we're on.
995
			// Some pages are not valid targets.
996
			if ( $rt->isValidRedirectTarget() ) {
997
				return $rt->getFullURL();
998
			} else {
999
				return false;
1000
			}
1001
		}
1002
1003
		return $rt;
1004
	}
1005
1006
	/**
1007
	 * Get a list of users who have edited this article, not including the user who made
1008
	 * the most recent revision, which you can get from $article->getUser() if you want it
1009
	 * @return UserArrayFromResult
1010
	 */
1011
	public function getContributors() {
1012
		// @todo FIXME: This is expensive; cache this info somewhere.
1013
1014
		$dbr = wfGetDB( DB_REPLICA );
1015
1016
		if ( $dbr->implicitGroupby() ) {
1017
			$realNameField = 'user_real_name';
1018
		} else {
1019
			$realNameField = 'MIN(user_real_name) AS user_real_name';
1020
		}
1021
1022
		$tables = [ 'revision', 'user' ];
1023
1024
		$fields = [
1025
			'user_id' => 'rev_user',
1026
			'user_name' => 'rev_user_text',
1027
			$realNameField,
1028
			'timestamp' => 'MAX(rev_timestamp)',
1029
		];
1030
1031
		$conds = [ 'rev_page' => $this->getId() ];
1032
1033
		// The user who made the top revision gets credited as "this page was last edited by
1034
		// John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1035
		$user = $this->getUser();
1036
		if ( $user ) {
1037
			$conds[] = "rev_user != $user";
1038
		} else {
1039
			$conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1040
		}
1041
1042
		// Username hidden?
1043
		$conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1044
1045
		$jconds = [
1046
			'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1047
		];
1048
1049
		$options = [
1050
			'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1051
			'ORDER BY' => 'timestamp DESC',
1052
		];
1053
1054
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1055
		return new UserArrayFromResult( $res );
1056
	}
1057
1058
	/**
1059
	 * Should the parser cache be used?
1060
	 *
1061
	 * @param ParserOptions $parserOptions ParserOptions to check
1062
	 * @param int $oldId
1063
	 * @return bool
1064
	 */
1065
	public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1066
		return $parserOptions->getStubThreshold() == 0
1067
			&& $this->exists()
1068
			&& ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1069
			&& $this->getContentHandler()->isParserCacheSupported();
1070
	}
1071
1072
	/**
1073
	 * Get a ParserOutput for the given ParserOptions and revision ID.
1074
	 *
1075
	 * The parser cache will be used if possible. Cache misses that result
1076
	 * in parser runs are debounced with PoolCounter.
1077
	 *
1078
	 * @since 1.19
1079
	 * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
1080
	 * @param null|int      $oldid Revision ID to get the text from, passing null or 0 will
1081
	 *                             get the current revision (default value)
1082
	 * @param bool          $forceParse Force reindexing, regardless of cache settings
1083
	 * @return bool|ParserOutput ParserOutput or false if the revision was not found
1084
	 */
1085
	public function getParserOutput(
1086
		ParserOptions $parserOptions, $oldid = null, $forceParse = false
1087
	) {
1088
		$useParserCache =
1089
			( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1090
		wfDebug( __METHOD__ .
1091
			': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1092
		if ( $parserOptions->getStubThreshold() ) {
1093
			wfIncrStats( 'pcache.miss.stub' );
1094
		}
1095
1096
		if ( $useParserCache ) {
1097
			$parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1098
			if ( $parserOutput !== false ) {
1099
				return $parserOutput;
1100
			}
1101
		}
1102
1103
		if ( $oldid === null || $oldid === 0 ) {
1104
			$oldid = $this->getLatest();
1105
		}
1106
1107
		$pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1108
		$pool->execute();
1109
1110
		return $pool->getParserOutput();
1111
	}
1112
1113
	/**
1114
	 * Do standard deferred updates after page view (existing or missing page)
1115
	 * @param User $user The relevant user
1116
	 * @param int $oldid Revision id being viewed; if not given or 0, latest revision is assumed
1117
	 */
1118
	public function doViewUpdates( User $user, $oldid = 0 ) {
1119
		if ( wfReadOnly() ) {
1120
			return;
1121
		}
1122
1123
		Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1124
		// Update newtalk / watchlist notification status
1125
		try {
1126
			$user->clearNotification( $this->mTitle, $oldid );
1127
		} catch ( DBError $e ) {
1128
			// Avoid outage if the master is not reachable
1129
			MWExceptionHandler::logException( $e );
1130
		}
1131
	}
1132
1133
	/**
1134
	 * Perform the actions of a page purging
1135
	 * @param integer $flags Bitfield of WikiPage::PURGE_* constants
1136
	 * @return bool
1137
	 */
1138
	public function doPurge( $flags = self::PURGE_ALL ) {
1139
		if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
1140
			return false;
1141
		}
1142
1143
		if ( ( $flags & self::PURGE_GLOBAL_PCACHE ) == self::PURGE_GLOBAL_PCACHE ) {
1144
			// Set page_touched in the database to invalidate all DC caches
1145
			$this->mTitle->invalidateCache();
1146
		} elseif ( ( $flags & self::PURGE_CLUSTER_PCACHE ) == self::PURGE_CLUSTER_PCACHE ) {
1147
			// Delete the parser options key in the local cluster to invalidate the DC cache
1148
			ParserCache::singleton()->deleteOptionsKey( $this );
1149
			// Avoid sending HTTP 304s in ViewAction to the client who just issued the purge
1150
			$cache = ObjectCache::getLocalClusterInstance();
1151
			$cache->set(
1152
				$cache->makeKey( 'page', 'last-dc-purge', $this->getId() ),
1153
				wfTimestamp( TS_MW ),
1154
				$cache::TTL_HOUR
1155
			);
1156
		}
1157
1158
		if ( ( $flags & self::PURGE_CDN_CACHE ) == self::PURGE_CDN_CACHE ) {
1159
			// Clear any HTML file cache
1160
			HTMLFileCache::clearFileCache( $this->getTitle() );
1161
			// Send purge after any page_touched above update was committed
1162
			DeferredUpdates::addUpdate(
1163
				new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1164
				DeferredUpdates::PRESEND
1165
			);
1166
		}
1167
1168
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1169
			// @todo move this logic to MessageCache
1170
			if ( $this->exists() ) {
1171
				// NOTE: use transclusion text for messages.
1172
				//       This is consistent with  MessageCache::getMsgFromNamespace()
1173
1174
				$content = $this->getContent();
1175
				$text = $content === null ? null : $content->getWikitextForTransclusion();
1176
1177
				if ( $text === null ) {
1178
					$text = false;
1179
				}
1180
			} else {
1181
				$text = false;
1182
			}
1183
1184
			MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1185
		}
1186
1187
		return true;
1188
	}
1189
1190
	/**
1191
	 * Get the last time a user explicitly purged the page via action=purge
1192
	 *
1193
	 * @return string|bool TS_MW timestamp or false
1194
	 * @since 1.28
1195
	 */
1196
	public function getLastPurgeTimestamp() {
1197
		$cache = ObjectCache::getLocalClusterInstance();
1198
1199
		return $cache->get( $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ) );
1200
	}
1201
1202
	/**
1203
	 * Insert a new empty page record for this article.
1204
	 * This *must* be followed up by creating a revision
1205
	 * and running $this->updateRevisionOn( ... );
1206
	 * or else the record will be left in a funky state.
1207
	 * Best if all done inside a transaction.
1208
	 *
1209
	 * @param IDatabase $dbw
1210
	 * @param int|null $pageId Custom page ID that will be used for the insert statement
1211
	 *
1212
	 * @return bool|int The newly created page_id key; false if the row was not
1213
	 *   inserted, e.g. because the title already existed or because the specified
1214
	 *   page ID is already in use.
1215
	 */
1216
	public function insertOn( $dbw, $pageId = null ) {
1217
		$pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
1218
		$dbw->insert(
1219
			'page',
1220
			[
1221
				'page_id'           => $pageIdForInsert,
1222
				'page_namespace'    => $this->mTitle->getNamespace(),
1223
				'page_title'        => $this->mTitle->getDBkey(),
1224
				'page_restrictions' => '',
1225
				'page_is_redirect'  => 0, // Will set this shortly...
1226
				'page_is_new'       => 1,
1227
				'page_random'       => wfRandom(),
1228
				'page_touched'      => $dbw->timestamp(),
1229
				'page_latest'       => 0, // Fill this in shortly...
1230
				'page_len'          => 0, // Fill this in shortly...
1231
			],
1232
			__METHOD__,
1233
			'IGNORE'
1234
		);
1235
1236
		if ( $dbw->affectedRows() > 0 ) {
1237
			$newid = $pageId ?: $dbw->insertId();
1238
			$this->mId = $newid;
1239
			$this->mTitle->resetArticleID( $newid );
1240
1241
			return $newid;
1242
		} else {
1243
			return false; // nothing changed
1244
		}
1245
	}
1246
1247
	/**
1248
	 * Update the page record to point to a newly saved revision.
1249
	 *
1250
	 * @param IDatabase $dbw
1251
	 * @param Revision $revision For ID number, and text used to set
1252
	 *   length and redirect status fields
1253
	 * @param int $lastRevision If given, will not overwrite the page field
1254
	 *   when different from the currently set value.
1255
	 *   Giving 0 indicates the new page flag should be set on.
1256
	 * @param bool $lastRevIsRedirect If given, will optimize adding and
1257
	 *   removing rows in redirect table.
1258
	 * @return bool Success; false if the page row was missing or page_latest changed
1259
	 */
1260
	public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1261
		$lastRevIsRedirect = null
1262
	) {
1263
		global $wgContentHandlerUseDB;
1264
1265
		// Assertion to try to catch T92046
1266
		if ( (int)$revision->getId() === 0 ) {
1267
			throw new InvalidArgumentException(
1268
				__METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1269
			);
1270
		}
1271
1272
		$content = $revision->getContent();
1273
		$len = $content ? $content->getSize() : 0;
1274
		$rt = $content ? $content->getUltimateRedirectTarget() : null;
1275
1276
		$conditions = [ 'page_id' => $this->getId() ];
1277
1278
		if ( !is_null( $lastRevision ) ) {
1279
			// An extra check against threads stepping on each other
1280
			$conditions['page_latest'] = $lastRevision;
1281
		}
1282
1283
		$row = [ /* SET */
1284
			'page_latest'      => $revision->getId(),
1285
			'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
1286
			'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
1287
			'page_is_redirect' => $rt !== null ? 1 : 0,
1288
			'page_len'         => $len,
1289
		];
1290
1291
		if ( $wgContentHandlerUseDB ) {
1292
			$row['page_content_model'] = $revision->getContentModel();
1293
		}
1294
1295
		$dbw->update( 'page',
1296
			$row,
1297
			$conditions,
1298
			__METHOD__ );
1299
1300
		$result = $dbw->affectedRows() > 0;
1301
		if ( $result ) {
1302
			$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 1274 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...
1303
			$this->setLastEdit( $revision );
1304
			$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...
1305
			$this->mIsRedirect = (bool)$rt;
1306
			// Update the LinkCache.
1307
			LinkCache::singleton()->addGoodLinkObj(
1308
				$this->getId(),
1309
				$this->mTitle,
1310
				$len,
1311
				$this->mIsRedirect,
1312
				$this->mLatest,
1313
				$revision->getContentModel()
1314
			);
1315
		}
1316
1317
		return $result;
1318
	}
1319
1320
	/**
1321
	 * Add row to the redirect table if this is a redirect, remove otherwise.
1322
	 *
1323
	 * @param IDatabase $dbw
1324
	 * @param Title $redirectTitle Title object pointing to the redirect target,
1325
	 *   or NULL if this is not a redirect
1326
	 * @param null|bool $lastRevIsRedirect If given, will optimize adding and
1327
	 *   removing rows in redirect table.
1328
	 * @return bool True on success, false on failure
1329
	 * @private
1330
	 */
1331
	public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1332
		// Always update redirects (target link might have changed)
1333
		// Update/Insert if we don't know if the last revision was a redirect or not
1334
		// Delete if changing from redirect to non-redirect
1335
		$isRedirect = !is_null( $redirectTitle );
1336
1337
		if ( !$isRedirect && $lastRevIsRedirect === false ) {
1338
			return true;
1339
		}
1340
1341
		if ( $isRedirect ) {
1342
			$this->insertRedirectEntry( $redirectTitle );
1343
		} else {
1344
			// This is not a redirect, remove row from redirect table
1345
			$where = [ 'rd_from' => $this->getId() ];
1346
			$dbw->delete( 'redirect', $where, __METHOD__ );
1347
		}
1348
1349
		if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1350
			RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1351
		}
1352
1353
		return ( $dbw->affectedRows() != 0 );
1354
	}
1355
1356
	/**
1357
	 * If the given revision is newer than the currently set page_latest,
1358
	 * update the page record. Otherwise, do nothing.
1359
	 *
1360
	 * @deprecated since 1.24, use updateRevisionOn instead
1361
	 *
1362
	 * @param IDatabase $dbw
1363
	 * @param Revision $revision
1364
	 * @return bool
1365
	 */
1366
	public function updateIfNewerOn( $dbw, $revision ) {
1367
1368
		$row = $dbw->selectRow(
1369
			[ 'revision', 'page' ],
1370
			[ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1371
			[
1372
				'page_id' => $this->getId(),
1373
				'page_latest=rev_id' ],
1374
			__METHOD__ );
1375
1376
		if ( $row ) {
1377
			if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1378
				return false;
1379
			}
1380
			$prev = $row->rev_id;
1381
			$lastRevIsRedirect = (bool)$row->page_is_redirect;
1382
		} else {
1383
			// No or missing previous revision; mark the page as new
1384
			$prev = 0;
1385
			$lastRevIsRedirect = null;
1386
		}
1387
1388
		$ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1389
1390
		return $ret;
1391
	}
1392
1393
	/**
1394
	 * Get the content that needs to be saved in order to undo all revisions
1395
	 * between $undo and $undoafter. Revisions must belong to the same page,
1396
	 * must exist and must not be deleted
1397
	 * @param Revision $undo
1398
	 * @param Revision $undoafter Must be an earlier revision than $undo
1399
	 * @return Content|bool Content on success, false on failure
1400
	 * @since 1.21
1401
	 * Before we had the Content object, this was done in getUndoText
1402
	 */
1403
	public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1404
		$handler = $undo->getContentHandler();
1405
		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 1403 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...
1406
	}
1407
1408
	/**
1409
	 * Returns true if this page's content model supports sections.
1410
	 *
1411
	 * @return bool
1412
	 *
1413
	 * @todo The skin should check this and not offer section functionality if
1414
	 *   sections are not supported.
1415
	 * @todo The EditPage should check this and not offer section functionality
1416
	 *   if sections are not supported.
1417
	 */
1418
	public function supportsSections() {
1419
		return $this->getContentHandler()->supportsSections();
1420
	}
1421
1422
	/**
1423
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1424
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1425
	 * or 'new' for a new section.
1426
	 * @param Content $sectionContent New content of the section.
1427
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1428
	 * @param string $edittime Revision timestamp or null to use the current revision.
1429
	 *
1430
	 * @throws MWException
1431
	 * @return Content|null New complete article content, or null if error.
1432
	 *
1433
	 * @since 1.21
1434
	 * @deprecated since 1.24, use replaceSectionAtRev instead
1435
	 */
1436
	public function replaceSectionContent(
1437
		$sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1438
	) {
1439
1440
		$baseRevId = null;
1441
		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...
1442
			$dbr = wfGetDB( DB_REPLICA );
1443
			$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
0 ignored issues
show
Bug introduced by
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 1442 can be null; however, Revision::loadFromTimestamp() 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...
1444
			// Try the master if this thread may have just added it.
1445
			// This could be abstracted into a Revision method, but we don't want
1446
			// to encourage loading of revisions by timestamp.
1447
			if ( !$rev
1448
				&& 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...
1449
				&& 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...
1450
			) {
1451
				$dbw = wfGetDB( DB_MASTER );
1452
				$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1451 can be null; however, Revision::loadFromTimestamp() 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...
1453
			}
1454
			if ( $rev ) {
1455
				$baseRevId = $rev->getId();
1456
			}
1457
		}
1458
1459
		return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1460
	}
1461
1462
	/**
1463
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1464
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1465
	 * or 'new' for a new section.
1466
	 * @param Content $sectionContent New content of the section.
1467
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1468
	 * @param int|null $baseRevId
1469
	 *
1470
	 * @throws MWException
1471
	 * @return Content|null New complete article content, or null if error.
1472
	 *
1473
	 * @since 1.24
1474
	 */
1475
	public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1476
		$sectionTitle = '', $baseRevId = null
1477
	) {
1478
1479
		if ( strval( $sectionId ) === '' ) {
1480
			// Whole-page edit; let the whole text through
1481
			$newContent = $sectionContent;
1482
		} else {
1483
			if ( !$this->supportsSections() ) {
1484
				throw new MWException( "sections not supported for content model " .
1485
					$this->getContentHandler()->getModelID() );
1486
			}
1487
1488
			// Bug 30711: always use current version when adding a new section
1489
			if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1490
				$oldContent = $this->getContent();
1491
			} else {
1492
				$rev = Revision::newFromId( $baseRevId );
1493
				if ( !$rev ) {
1494
					wfDebug( __METHOD__ . " asked for bogus section (page: " .
1495
						$this->getId() . "; section: $sectionId)\n" );
1496
					return null;
1497
				}
1498
1499
				$oldContent = $rev->getContent();
1500
			}
1501
1502
			if ( !$oldContent ) {
1503
				wfDebug( __METHOD__ . ": no page text\n" );
1504
				return null;
1505
			}
1506
1507
			$newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1508
		}
1509
1510
		return $newContent;
1511
	}
1512
1513
	/**
1514
	 * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
1515
	 * @param int $flags
1516
	 * @return int Updated $flags
1517
	 */
1518
	public function checkFlags( $flags ) {
1519
		if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1520
			if ( $this->exists() ) {
1521
				$flags |= EDIT_UPDATE;
1522
			} else {
1523
				$flags |= EDIT_NEW;
1524
			}
1525
		}
1526
1527
		return $flags;
1528
	}
1529
1530
	/**
1531
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1532
	 * optionally via the deferred update array.
1533
	 *
1534
	 * @param string $text New text
1535
	 * @param string $summary Edit summary
1536
	 * @param int $flags Bitfield:
1537
	 *      EDIT_NEW
1538
	 *          Article is known or assumed to be non-existent, create a new one
1539
	 *      EDIT_UPDATE
1540
	 *          Article is known or assumed to be pre-existing, update it
1541
	 *      EDIT_MINOR
1542
	 *          Mark this edit minor, if the user is allowed to do so
1543
	 *      EDIT_SUPPRESS_RC
1544
	 *          Do not log the change in recentchanges
1545
	 *      EDIT_FORCE_BOT
1546
	 *          Mark the edit a "bot" edit regardless of user rights
1547
	 *      EDIT_AUTOSUMMARY
1548
	 *          Fill in blank summaries with generated text where possible
1549
	 *      EDIT_INTERNAL
1550
	 *          Signal that the page retrieve/save cycle happened entirely in this request.
1551
	 *
1552
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1553
	 * article will be detected. If EDIT_UPDATE is specified and the article
1554
	 * doesn't exist, the function will return an edit-gone-missing error. If
1555
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1556
	 * error will be returned. These two conditions are also possible with
1557
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1558
	 *
1559
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1560
	 *   This is not the parent revision ID, rather the revision ID for older
1561
	 *   content used as the source for a rollback, for example.
1562
	 * @param User $user The user doing the edit
1563
	 *
1564
	 * @throws MWException
1565
	 * @return Status Possible errors:
1566
	 *   edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1567
	 *     set the fatal flag of $status
1568
	 *   edit-gone-missing: In update mode, but the article didn't exist.
1569
	 *   edit-conflict: In update mode, the article changed unexpectedly.
1570
	 *   edit-no-change: Warning that the text was the same as before.
1571
	 *   edit-already-exists: In creation mode, but the article already exists.
1572
	 *
1573
	 * Extensions may define additional errors.
1574
	 *
1575
	 * $return->value will contain an associative array with members as follows:
1576
	 *     new: Boolean indicating if the function attempted to create a new article.
1577
	 *     revision: The revision object for the inserted revision, or null.
1578
	 *
1579
	 * Compatibility note: this function previously returned a boolean value
1580
	 * indicating success/failure
1581
	 *
1582
	 * @deprecated since 1.21: use doEditContent() instead.
1583
	 */
1584
	public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1585
		wfDeprecated( __METHOD__, '1.21' );
1586
1587
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
1588
1589
		return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1590
	}
1591
1592
	/**
1593
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1594
	 * optionally via the deferred update array.
1595
	 *
1596
	 * @param Content $content New content
1597
	 * @param string $summary Edit summary
1598
	 * @param int $flags Bitfield:
1599
	 *      EDIT_NEW
1600
	 *          Article is known or assumed to be non-existent, create a new one
1601
	 *      EDIT_UPDATE
1602
	 *          Article is known or assumed to be pre-existing, update it
1603
	 *      EDIT_MINOR
1604
	 *          Mark this edit minor, if the user is allowed to do so
1605
	 *      EDIT_SUPPRESS_RC
1606
	 *          Do not log the change in recentchanges
1607
	 *      EDIT_FORCE_BOT
1608
	 *          Mark the edit a "bot" edit regardless of user rights
1609
	 *      EDIT_AUTOSUMMARY
1610
	 *          Fill in blank summaries with generated text where possible
1611
	 *      EDIT_INTERNAL
1612
	 *          Signal that the page retrieve/save cycle happened entirely in this request.
1613
	 *
1614
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1615
	 * article will be detected. If EDIT_UPDATE is specified and the article
1616
	 * doesn't exist, the function will return an edit-gone-missing error. If
1617
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1618
	 * error will be returned. These two conditions are also possible with
1619
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1620
	 *
1621
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1622
	 *   This is not the parent revision ID, rather the revision ID for older
1623
	 *   content used as the source for a rollback, for example.
1624
	 * @param User $user The user doing the edit
1625
	 * @param string $serialFormat Format for storing the content in the
1626
	 *   database.
1627
	 * @param array|null $tags Change tags to apply to this edit
1628
	 * Callers are responsible for permission checks
1629
	 * (with ChangeTags::canAddTagsAccompanyingChange)
1630
	 *
1631
	 * @throws MWException
1632
	 * @return Status Possible errors:
1633
	 *     edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1634
	 *       set the fatal flag of $status.
1635
	 *     edit-gone-missing: In update mode, but the article didn't exist.
1636
	 *     edit-conflict: In update mode, the article changed unexpectedly.
1637
	 *     edit-no-change: Warning that the text was the same as before.
1638
	 *     edit-already-exists: In creation mode, but the article already exists.
1639
	 *
1640
	 *  Extensions may define additional errors.
1641
	 *
1642
	 *  $return->value will contain an associative array with members as follows:
1643
	 *     new: Boolean indicating if the function attempted to create a new article.
1644
	 *     revision: The revision object for the inserted revision, or null.
1645
	 *
1646
	 * @since 1.21
1647
	 * @throws MWException
1648
	 */
1649
	public function doEditContent(
1650
		Content $content, $summary, $flags = 0, $baseRevId = false,
1651
		User $user = null, $serialFormat = null, $tags = []
1652
	) {
1653
		global $wgUser, $wgUseAutomaticEditSummaries;
1654
1655
		// Old default parameter for $tags was null
1656
		if ( $tags === null ) {
1657
			$tags = [];
1658
		}
1659
1660
		// Low-level sanity check
1661
		if ( $this->mTitle->getText() === '' ) {
1662
			throw new MWException( 'Something is trying to edit an article with an empty title' );
1663
		}
1664
		// Make sure the given content type is allowed for this page
1665
		if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1666
			return Status::newFatal( 'content-not-allowed-here',
1667
				ContentHandler::getLocalizedName( $content->getModel() ),
1668
				$this->mTitle->getPrefixedText()
1669
			);
1670
		}
1671
1672
		// Load the data from the master database if needed.
1673
		// The caller may already loaded it from the master or even loaded it using
1674
		// SELECT FOR UPDATE, so do not override that using clear().
1675
		$this->loadPageData( 'fromdbmaster' );
1676
1677
		$user = $user ?: $wgUser;
1678
		$flags = $this->checkFlags( $flags );
1679
1680
		// Trigger pre-save hook (using provided edit summary)
1681
		$hookStatus = Status::newGood( [] );
1682
		$hook_args = [ &$this, &$user, &$content, &$summary,
1683
							$flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1684
		// Check if the hook rejected the attempted save
1685
		if ( !Hooks::run( 'PageContentSave', $hook_args )
1686
			|| !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args, '1.21' )
1687
		) {
1688
			if ( $hookStatus->isOK() ) {
1689
				// Hook returned false but didn't call fatal(); use generic message
1690
				$hookStatus->fatal( 'edit-hook-aborted' );
1691
			}
1692
1693
			return $hookStatus;
1694
		}
1695
1696
		$old_revision = $this->getRevision(); // current revision
1697
		$old_content = $this->getContent( Revision::RAW ); // current revision's content
1698
1699
		if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
1700
			$tags[] = 'mw-contentmodelchange';
1701
		}
1702
1703
		// Provide autosummaries if one is not provided and autosummaries are enabled
1704
		if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1705
			$handler = $content->getContentHandler();
1706
			$summary = $handler->getAutosummary( $old_content, $content, $flags );
1707
		}
1708
1709
		// Avoid statsd noise and wasted cycles check the edit stash (T136678)
1710
		if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
1711
			$useCache = false;
1712
		} else {
1713
			$useCache = true;
1714
		}
1715
1716
		// Get the pre-save transform content and final parser output
1717
		$editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
1718
		$pstContent = $editInfo->pstContent; // Content object
1719
		$meta = [
1720
			'bot' => ( $flags & EDIT_FORCE_BOT ),
1721
			'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1722
			'serialized' => $editInfo->pst,
1723
			'serialFormat' => $serialFormat,
1724
			'baseRevId' => $baseRevId,
1725
			'oldRevision' => $old_revision,
1726
			'oldContent' => $old_content,
1727
			'oldId' => $this->getLatest(),
1728
			'oldIsRedirect' => $this->isRedirect(),
1729
			'oldCountable' => $this->isCountable(),
1730
			'tags' => ( $tags !== null ) ? (array)$tags : []
1731
		];
1732
1733
		// Actually create the revision and create/update the page
1734
		if ( $flags & EDIT_UPDATE ) {
1735
			$status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1736
		} else {
1737
			$status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1738
		}
1739
1740
		// Promote user to any groups they meet the criteria for
1741
		DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1742
			$user->addAutopromoteOnceGroups( 'onEdit' );
1743
			$user->addAutopromoteOnceGroups( 'onView' ); // b/c
1744
		} );
1745
1746
		return $status;
1747
	}
1748
1749
	/**
1750
	 * @param Content $content Pre-save transform content
1751
	 * @param integer $flags
1752
	 * @param User $user
1753
	 * @param string $summary
1754
	 * @param array $meta
1755
	 * @return Status
1756
	 * @throws DBUnexpectedError
1757
	 * @throws Exception
1758
	 * @throws FatalError
1759
	 * @throws MWException
1760
	 */
1761
	private function doModify(
1762
		Content $content, $flags, User $user, $summary, array $meta
1763
	) {
1764
		global $wgUseRCPatrol;
1765
1766
		// Update article, but only if changed.
1767
		$status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1768
1769
		// Convenience variables
1770
		$now = wfTimestampNow();
1771
		$oldid = $meta['oldId'];
1772
		/** @var $oldContent Content|null */
1773
		$oldContent = $meta['oldContent'];
1774
		$newsize = $content->getSize();
1775
1776
		if ( !$oldid ) {
1777
			// Article gone missing
1778
			$status->fatal( 'edit-gone-missing' );
1779
1780
			return $status;
1781
		} elseif ( !$oldContent ) {
1782
			// Sanity check for bug 37225
1783
			throw new MWException( "Could not find text for current revision {$oldid}." );
1784
		}
1785
1786
		// @TODO: pass content object?!
1787
		$revision = new Revision( [
1788
			'page'       => $this->getId(),
1789
			'title'      => $this->mTitle, // for determining the default content model
1790
			'comment'    => $summary,
1791
			'minor_edit' => $meta['minor'],
1792
			'text'       => $meta['serialized'],
1793
			'len'        => $newsize,
1794
			'parent_id'  => $oldid,
1795
			'user'       => $user->getId(),
1796
			'user_text'  => $user->getName(),
1797
			'timestamp'  => $now,
1798
			'content_model' => $content->getModel(),
1799
			'content_format' => $meta['serialFormat'],
1800
		] );
1801
1802
		$changed = !$content->equals( $oldContent );
1803
1804
		$dbw = wfGetDB( DB_MASTER );
1805
1806
		if ( $changed ) {
1807
			$prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1808
			$status->merge( $prepStatus );
1809
			if ( !$status->isOK() ) {
1810
				return $status;
1811
			}
1812
1813
			$dbw->startAtomic( __METHOD__ );
1814
			// Get the latest page_latest value while locking it.
1815
			// Do a CAS style check to see if it's the same as when this method
1816
			// started. If it changed then bail out before touching the DB.
1817
			$latestNow = $this->lockAndGetLatest();
1818
			if ( $latestNow != $oldid ) {
1819
				$dbw->endAtomic( __METHOD__ );
1820
				// Page updated or deleted in the mean time
1821
				$status->fatal( 'edit-conflict' );
1822
1823
				return $status;
1824
			}
1825
1826
			// At this point we are now comitted to returning an OK
1827
			// status unless some DB query error or other exception comes up.
1828
			// This way callers don't have to call rollback() if $status is bad
1829
			// unless they actually try to catch exceptions (which is rare).
1830
1831
			// Save the revision text
1832
			$revisionId = $revision->insertOn( $dbw );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1804 can be null; however, Revision::insertOn() 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...
1833
			// Update page_latest and friends to reflect the new revision
1834
			if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1804 can be null; however, WikiPage::updateRevisionOn() 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...
1835
				throw new MWException( "Failed to update page row to use new revision." );
1836
			}
1837
1838
			Hooks::run( 'NewRevisionFromEditComplete',
1839
				[ $this, $revision, $meta['baseRevId'], $user ] );
1840
1841
			// Update recentchanges
1842
			if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1843
				// Mark as patrolled if the user can do so
1844
				$patrolled = $wgUseRCPatrol && !count(
1845
						$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1846
				// Add RC row to the DB
1847
				RecentChange::notifyEdit(
1848
					$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1770 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...
1849
					$this->mTitle,
1850
					$revision->isMinor(),
1851
					$user,
1852
					$summary,
1853
					$oldid,
1854
					$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...
1855
					$meta['bot'],
1856
					'',
1857
					$oldContent ? $oldContent->getSize() : 0,
1858
					$newsize,
1859
					$revisionId,
1860
					$patrolled,
1861
					$meta['tags']
1862
				);
1863
			}
1864
1865
			$user->incEditCount();
1866
1867
			$dbw->endAtomic( __METHOD__ );
1868
			$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...
1869
		} else {
1870
			// Bug 32948: revision ID must be set to page {{REVISIONID}} and
1871
			// related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1872
			$revision->setId( $this->getLatest() );
1873
			$revision->setUserIdAndName(
1874
				$this->getUser( Revision::RAW ),
1875
				$this->getUserText( Revision::RAW )
0 ignored issues
show
Bug introduced by
It seems like $this->getUserText(\Revision::RAW) targeting WikiPage::getUserText() can also be of type boolean; however, Revision::setUserIdAndName() does only seem to accept string, maybe add an additional type check?

This check looks at variables that 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...
1876
			);
1877
		}
1878
1879
		if ( $changed ) {
1880
			// Return the new revision to the caller
1881
			$status->value['revision'] = $revision;
1882
		} else {
1883
			$status->warning( 'edit-no-change' );
1884
			// Update page_touched as updateRevisionOn() was not called.
1885
			// Other cache updates are managed in onArticleEdit() via doEditUpdates().
1886
			$this->mTitle->invalidateCache( $now );
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1770 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...
1887
		}
1888
1889
		// Do secondary updates once the main changes have been committed...
1890
		DeferredUpdates::addUpdate(
1891
			new AtomicSectionUpdate(
1892
				$dbw,
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1804 can be null; however, AtomicSectionUpdate::__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...
1893
				__METHOD__,
1894
				function () use (
1895
					$revision, &$user, $content, $summary, &$flags,
1896
					$changed, $meta, &$status
1897
				) {
1898
					// Update links tables, site stats, etc.
1899
					$this->doEditUpdates(
1900
						$revision,
1901
						$user,
1902
						[
1903
							'changed' => $changed,
1904
							'oldcountable' => $meta['oldCountable'],
1905
							'oldrevision' => $meta['oldRevision']
1906
						]
1907
					);
1908
					// Trigger post-save hook
1909
					$params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
1910
						null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
1911
					ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1912
					Hooks::run( 'PageContentSaveComplete', $params );
1913
				}
1914
			),
1915
			DeferredUpdates::PRESEND
1916
		);
1917
1918
		return $status;
1919
	}
1920
1921
	/**
1922
	 * @param Content $content Pre-save transform content
1923
	 * @param integer $flags
1924
	 * @param User $user
1925
	 * @param string $summary
1926
	 * @param array $meta
1927
	 * @return Status
1928
	 * @throws DBUnexpectedError
1929
	 * @throws Exception
1930
	 * @throws FatalError
1931
	 * @throws MWException
1932
	 */
1933
	private function doCreate(
1934
		Content $content, $flags, User $user, $summary, array $meta
1935
	) {
1936
		global $wgUseRCPatrol, $wgUseNPPatrol;
1937
1938
		$status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1939
1940
		$now = wfTimestampNow();
1941
		$newsize = $content->getSize();
1942
		$prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1943
		$status->merge( $prepStatus );
1944
		if ( !$status->isOK() ) {
1945
			return $status;
1946
		}
1947
1948
		$dbw = wfGetDB( DB_MASTER );
1949
		$dbw->startAtomic( __METHOD__ );
1950
1951
		// Add the page record unless one already exists for the title
1952
		$newid = $this->insertOn( $dbw );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1948 can be null; however, WikiPage::insertOn() 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...
1953
		if ( $newid === false ) {
1954
			$dbw->endAtomic( __METHOD__ ); // nothing inserted
1955
			$status->fatal( 'edit-already-exists' );
1956
1957
			return $status; // nothing done
1958
		}
1959
1960
		// At this point we are now comitted to returning an OK
1961
		// status unless some DB query error or other exception comes up.
1962
		// This way callers don't have to call rollback() if $status is bad
1963
		// unless they actually try to catch exceptions (which is rare).
1964
1965
		// @TODO: pass content object?!
1966
		$revision = new Revision( [
1967
			'page'       => $newid,
1968
			'title'      => $this->mTitle, // for determining the default content model
1969
			'comment'    => $summary,
1970
			'minor_edit' => $meta['minor'],
1971
			'text'       => $meta['serialized'],
1972
			'len'        => $newsize,
1973
			'user'       => $user->getId(),
1974
			'user_text'  => $user->getName(),
1975
			'timestamp'  => $now,
1976
			'content_model' => $content->getModel(),
1977
			'content_format' => $meta['serialFormat'],
1978
		] );
1979
1980
		// Save the revision text...
1981
		$revisionId = $revision->insertOn( $dbw );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1948 can be null; however, Revision::insertOn() 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...
1982
		// Update the page record with revision data
1983
		if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1948 can be null; however, WikiPage::updateRevisionOn() 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...
1984
			throw new MWException( "Failed to update page row to use new revision." );
1985
		}
1986
1987
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1988
1989
		// Update recentchanges
1990
		if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1991
			// Mark as patrolled if the user can do so
1992
			$patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1993
				!count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1994
			// Add RC row to the DB
1995
			RecentChange::notifyNew(
1996
				$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1940 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...
1997
				$this->mTitle,
1998
				$revision->isMinor(),
1999
				$user,
2000
				$summary,
2001
				$meta['bot'],
2002
				'',
2003
				$newsize,
2004
				$revisionId,
2005
				$patrolled,
2006
				$meta['tags']
2007
			);
2008
		}
2009
2010
		$user->incEditCount();
2011
2012
		$dbw->endAtomic( __METHOD__ );
2013
		$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...
2014
2015
		// Return the new revision to the caller
2016
		$status->value['revision'] = $revision;
2017
2018
		// Do secondary updates once the main changes have been committed...
2019
		DeferredUpdates::addUpdate(
2020
			new AtomicSectionUpdate(
2021
				$dbw,
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 1948 can be null; however, AtomicSectionUpdate::__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...
2022
				__METHOD__,
2023
				function () use (
2024
					$revision, &$user, $content, $summary, &$flags, $meta, &$status
2025
				) {
2026
					// Update links, etc.
2027
					$this->doEditUpdates( $revision, $user, [ 'created' => true ] );
2028
					// Trigger post-create hook
2029
					$params = [ &$this, &$user, $content, $summary,
2030
						$flags & EDIT_MINOR, null, null, &$flags, $revision ];
2031
					ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params, '1.21' );
2032
					Hooks::run( 'PageContentInsertComplete', $params );
2033
					// Trigger post-save hook
2034
					$params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
2035
					ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params, '1.21' );
2036
					Hooks::run( 'PageContentSaveComplete', $params );
2037
2038
				}
2039
			),
2040
			DeferredUpdates::PRESEND
2041
		);
2042
2043
		return $status;
2044
	}
2045
2046
	/**
2047
	 * Get parser options suitable for rendering the primary article wikitext
2048
	 *
2049
	 * @see ContentHandler::makeParserOptions
2050
	 *
2051
	 * @param IContextSource|User|string $context One of the following:
2052
	 *        - IContextSource: Use the User and the Language of the provided
2053
	 *          context
2054
	 *        - User: Use the provided User object and $wgLang for the language,
2055
	 *          so use an IContextSource object if possible.
2056
	 *        - 'canonical': Canonical options (anonymous user with default
2057
	 *          preferences and content language).
2058
	 * @return ParserOptions
2059
	 */
2060
	public function makeParserOptions( $context ) {
2061
		$options = $this->getContentHandler()->makeParserOptions( $context );
2062
2063
		if ( $this->getTitle()->isConversionTable() ) {
2064
			// @todo ConversionTable should become a separate content model, so
2065
			// we don't need special cases like this one.
2066
			$options->disableContentConversion();
2067
		}
2068
2069
		return $options;
2070
	}
2071
2072
	/**
2073
	 * Prepare text which is about to be saved.
2074
	 * Returns a stdClass with source, pst and output members
2075
	 *
2076
	 * @param string $text
2077
	 * @param int|null $revid
2078
	 * @param User|null $user
2079
	 * @deprecated since 1.21: use prepareContentForEdit instead.
2080
	 * @return object
2081
	 */
2082
	public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
2083
		wfDeprecated( __METHOD__, '1.21' );
2084
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
2085
		return $this->prepareContentForEdit( $content, $revid, $user );
2086
	}
2087
2088
	/**
2089
	 * Prepare content which is about to be saved.
2090
	 * Returns a stdClass with source, pst and output members
2091
	 *
2092
	 * @param Content $content
2093
	 * @param Revision|int|null $revision Revision object. For backwards compatibility, a
2094
	 *        revision ID is also accepted, but this is deprecated.
2095
	 * @param User|null $user
2096
	 * @param string|null $serialFormat
2097
	 * @param bool $useCache Check shared prepared edit cache
2098
	 *
2099
	 * @return object
2100
	 *
2101
	 * @since 1.21
2102
	 */
2103
	public function prepareContentForEdit(
2104
		Content $content, $revision = null, User $user = null,
2105
		$serialFormat = null, $useCache = true
2106
	) {
2107
		global $wgContLang, $wgUser, $wgAjaxEditStash;
2108
2109
		if ( is_object( $revision ) ) {
2110
			$revid = $revision->getId();
2111
		} else {
2112
			$revid = $revision;
2113
			// This code path is deprecated, and nothing is known to
2114
			// use it, so performance here shouldn't be a worry.
2115
			if ( $revid !== null ) {
2116
				$revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2117
			} else {
2118
				$revision = null;
2119
			}
2120
		}
2121
2122
		$user = is_null( $user ) ? $wgUser : $user;
2123
		// XXX: check $user->getId() here???
2124
2125
		// Use a sane default for $serialFormat, see bug 57026
2126
		if ( $serialFormat === null ) {
2127
			$serialFormat = $content->getContentHandler()->getDefaultFormat();
2128
		}
2129
2130
		if ( $this->mPreparedEdit
2131
			&& isset( $this->mPreparedEdit->newContent )
2132
			&& $this->mPreparedEdit->newContent->equals( $content )
2133
			&& $this->mPreparedEdit->revid == $revid
2134
			&& $this->mPreparedEdit->format == $serialFormat
2135
			// XXX: also check $user here?
2136
		) {
2137
			// Already prepared
2138
			return $this->mPreparedEdit;
2139
		}
2140
2141
		// The edit may have already been prepared via api.php?action=stashedit
2142
		$cachedEdit = $useCache && $wgAjaxEditStash
2143
			? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2144
			: false;
2145
2146
		$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2147
		Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2148
2149
		$edit = (object)[];
2150
		if ( $cachedEdit ) {
2151
			$edit->timestamp = $cachedEdit->timestamp;
2152
		} else {
2153
			$edit->timestamp = wfTimestampNow();
2154
		}
2155
		// @note: $cachedEdit is safely not used if the rev ID was referenced in the text
2156
		$edit->revid = $revid;
2157
2158
		if ( $cachedEdit ) {
2159
			$edit->pstContent = $cachedEdit->pstContent;
2160
		} else {
2161
			$edit->pstContent = $content
2162
				? $content->preSaveTransform( $this->mTitle, $user, $popts )
2163
				: null;
2164
		}
2165
2166
		$edit->format = $serialFormat;
2167
		$edit->popts = $this->makeParserOptions( 'canonical' );
2168
		if ( $cachedEdit ) {
2169
			$edit->output = $cachedEdit->output;
2170
		} else {
2171
			if ( $revision ) {
2172
				// We get here if vary-revision is set. This means that this page references
2173
				// itself (such as via self-transclusion). In this case, we need to make sure
2174
				// that any such self-references refer to the newly-saved revision, and not
2175
				// to the previous one, which could otherwise happen due to replica DB lag.
2176
				$oldCallback = $edit->popts->getCurrentRevisionCallback();
2177
				$edit->popts->setCurrentRevisionCallback(
2178
					function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2179
						if ( $title->equals( $revision->getTitle() ) ) {
2180
							return $revision;
2181
						} else {
2182
							return call_user_func( $oldCallback, $title, $parser );
2183
						}
2184
					}
2185
				);
2186
			} else {
2187
				// Try to avoid a second parse if {{REVISIONID}} is used
2188
				$edit->popts->setSpeculativeRevIdCallback( function () {
2189
					return 1 + (int)wfGetDB( DB_MASTER )->selectField(
2190
						'revision',
2191
						'MAX(rev_id)',
2192
						[],
2193
						__METHOD__
2194
					);
2195
				} );
2196
			}
2197
			$edit->output = $edit->pstContent
2198
				? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2199
				: null;
2200
		}
2201
2202
		$edit->newContent = $content;
2203
		$edit->oldContent = $this->getContent( Revision::RAW );
2204
2205
		// NOTE: B/C for hooks! don't use these fields!
2206
		$edit->newText = $edit->newContent
2207
			? ContentHandler::getContentText( $edit->newContent )
2208
			: '';
2209
		$edit->oldText = $edit->oldContent
2210
			? ContentHandler::getContentText( $edit->oldContent )
2211
			: '';
2212
		$edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2213
2214
		if ( $edit->output ) {
2215
			$edit->output->setCacheTime( wfTimestampNow() );
2216
		}
2217
2218
		// Process cache the result
2219
		$this->mPreparedEdit = $edit;
2220
2221
		return $edit;
2222
	}
2223
2224
	/**
2225
	 * Do standard deferred updates after page edit.
2226
	 * Update links tables, site stats, search index and message cache.
2227
	 * Purges pages that include this page if the text was changed here.
2228
	 * Every 100th edit, prune the recent changes table.
2229
	 *
2230
	 * @param Revision $revision
2231
	 * @param User $user User object that did the revision
2232
	 * @param array $options Array of options, following indexes are used:
2233
	 * - changed: boolean, whether the revision changed the content (default true)
2234
	 * - created: boolean, whether the revision created the page (default false)
2235
	 * - moved: boolean, whether the page was moved (default false)
2236
	 * - restored: boolean, whether the page was undeleted (default false)
2237
	 * - oldrevision: Revision object for the pre-update revision (default null)
2238
	 * - oldcountable: boolean, null, or string 'no-change' (default null):
2239
	 *   - boolean: whether the page was counted as an article before that
2240
	 *     revision, only used in changed is true and created is false
2241
	 *   - null: if created is false, don't update the article count; if created
2242
	 *     is true, do update the article count
2243
	 *   - 'no-change': don't update the article count, ever
2244
	 */
2245
	public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2246
		global $wgRCWatchCategoryMembership, $wgContLang;
2247
2248
		$options += [
2249
			'changed' => true,
2250
			'created' => false,
2251
			'moved' => false,
2252
			'restored' => false,
2253
			'oldrevision' => null,
2254
			'oldcountable' => null
2255
		];
2256
		$content = $revision->getContent();
2257
2258
		$logger = LoggerFactory::getInstance( 'SaveParse' );
2259
2260
		// See if the parser output before $revision was inserted is still valid
2261
		$editInfo = false;
2262
		if ( !$this->mPreparedEdit ) {
2263
			$logger->debug( __METHOD__ . ": No prepared edit...\n" );
2264
		} elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2265
			$logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
2266
		} elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
2267
			&& $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
2268
		) {
2269
			$logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
2270
		} elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
2271
			$logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
2272
		} else {
2273
			wfDebug( __METHOD__ . ": Using prepared edit...\n" );
2274
			$editInfo = $this->mPreparedEdit;
2275
		}
2276
2277
		if ( !$editInfo ) {
2278
			// Parse the text again if needed. Be careful not to do pre-save transform twice:
2279
			// $text is usually already pre-save transformed once. Avoid using the edit stash
2280
			// as any prepared content from there or in doEditContent() was already rejected.
2281
			$editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2256 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...
2282
		}
2283
2284
		// Save it to the parser cache.
2285
		// Make sure the cache time matches page_touched to avoid double parsing.
2286
		ParserCache::singleton()->save(
2287
			$editInfo->output, $this, $editInfo->popts,
2288
			$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...
2289
		);
2290
2291
		// Update the links tables and other secondary data
2292
		if ( $content ) {
2293
			$recursive = $options['changed']; // bug 50785
2294
			$updates = $content->getSecondaryDataUpdates(
2295
				$this->getTitle(), null, $recursive, $editInfo->output
2296
			);
2297
			foreach ( $updates as $update ) {
2298
				if ( $update instanceof LinksUpdate ) {
2299
					$update->setRevision( $revision );
2300
					$update->setTriggeringUser( $user );
2301
				}
2302
				DeferredUpdates::addUpdate( $update );
2303
			}
2304
			if ( $wgRCWatchCategoryMembership
2305
				&& $this->getContentHandler()->supportsCategories() === true
2306
				&& ( $options['changed'] || $options['created'] )
2307
				&& !$options['restored']
2308
			) {
2309
				// Note: jobs are pushed after deferred updates, so the job should be able to see
2310
				// the recent change entry (also done via deferred updates) and carry over any
2311
				// bot/deletion/IP flags, ect.
2312
				JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
2313
					$this->getTitle(),
2314
					[
2315
						'pageId' => $this->getId(),
2316
						'revTimestamp' => $revision->getTimestamp()
2317
					]
2318
				) );
2319
			}
2320
		}
2321
2322
		Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
2323
2324
		if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
2325
			// Flush old entries from the `recentchanges` table
2326
			if ( mt_rand( 0, 9 ) == 0 ) {
2327
				JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
2328
			}
2329
		}
2330
2331
		if ( !$this->exists() ) {
2332
			return;
2333
		}
2334
2335
		$id = $this->getId();
2336
		$title = $this->mTitle->getPrefixedDBkey();
2337
		$shortTitle = $this->mTitle->getDBkey();
2338
2339
		if ( $options['oldcountable'] === 'no-change' ||
2340
			( !$options['changed'] && !$options['moved'] )
2341
		) {
2342
			$good = 0;
2343
		} elseif ( $options['created'] ) {
2344
			$good = (int)$this->isCountable( $editInfo );
2345
		} elseif ( $options['oldcountable'] !== null ) {
2346
			$good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2347
		} else {
2348
			$good = 0;
2349
		}
2350
		$edits = $options['changed'] ? 1 : 0;
2351
		$total = $options['created'] ? 1 : 0;
2352
2353
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2354
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2256 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...
2355
2356
		// If this is another user's talk page, update newtalk.
2357
		// Don't do this if $options['changed'] = false (null-edits) nor if
2358
		// it's a minor edit and the user doesn't want notifications for those.
2359
		if ( $options['changed']
2360
			&& $this->mTitle->getNamespace() == NS_USER_TALK
2361
			&& $shortTitle != $user->getTitleKey()
2362
			&& !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2363
		) {
2364
			$recipient = User::newFromName( $shortTitle, false );
2365
			if ( !$recipient ) {
2366
				wfDebug( __METHOD__ . ": invalid username\n" );
2367
			} else {
2368
				// Allow extensions to prevent user notification
2369
				// when a new message is added to their talk page
2370
				if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
2371
					if ( User::isIP( $shortTitle ) ) {
2372
						// An anonymous user
2373
						$recipient->setNewtalk( true, $revision );
2374
					} elseif ( $recipient->isLoggedIn() ) {
2375
						$recipient->setNewtalk( true, $revision );
2376
					} else {
2377
						wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2378
					}
2379
				}
2380
			}
2381
		}
2382
2383
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2384
			// XXX: could skip pseudo-messages like js/css here, based on content model.
2385
			$msgtext = $content ? $content->getWikitextForTransclusion() : null;
2386
			if ( $msgtext === false || $msgtext === null ) {
2387
				$msgtext = '';
2388
			}
2389
2390
			MessageCache::singleton()->replace( $shortTitle, $msgtext );
2391
2392
			if ( $wgContLang->hasVariants() ) {
2393
				$wgContLang->updateConversionTable( $this->mTitle );
2394
			}
2395
		}
2396
2397
		if ( $options['created'] ) {
2398
			self::onArticleCreate( $this->mTitle );
2399
		} elseif ( $options['changed'] ) { // bug 50785
2400
			self::onArticleEdit( $this->mTitle, $revision );
2401
		}
2402
2403
		ResourceLoaderWikiModule::invalidateModuleCache(
2404
			$this->mTitle, $options['oldrevision'], $revision, wfWikiID()
2405
		);
2406
	}
2407
2408
	/**
2409
	 * Update the article's restriction field, and leave a log entry.
2410
	 * This works for protection both existing and non-existing pages.
2411
	 *
2412
	 * @param array $limit Set of restriction keys
2413
	 * @param array $expiry Per restriction type expiration
2414
	 * @param int &$cascade Set to false if cascading protection isn't allowed.
2415
	 * @param string $reason
2416
	 * @param User $user The user updating the restrictions
2417
	 * @param string|string[] $tags Change tags to add to the pages and protection log entries
2418
	 *   ($user should be able to add the specified tags before this is called)
2419
	 * @return Status Status object; if action is taken, $status->value is the log_id of the
2420
	 *   protection log entry.
2421
	 */
2422
	public function doUpdateRestrictions( array $limit, array $expiry,
2423
		&$cascade, $reason, User $user, $tags = null
2424
	) {
2425
		global $wgCascadingRestrictionLevels, $wgContLang;
2426
2427
		if ( wfReadOnly() ) {
2428
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2429
		}
2430
2431
		$this->loadPageData( 'fromdbmaster' );
2432
		$restrictionTypes = $this->mTitle->getRestrictionTypes();
2433
		$id = $this->getId();
2434
2435
		if ( !$cascade ) {
2436
			$cascade = false;
2437
		}
2438
2439
		// Take this opportunity to purge out expired restrictions
2440
		Title::purgeExpiredRestrictions();
2441
2442
		// @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2443
		// we expect a single selection, but the schema allows otherwise.
2444
		$isProtected = false;
2445
		$protect = false;
2446
		$changed = false;
2447
2448
		$dbw = wfGetDB( DB_MASTER );
2449
2450
		foreach ( $restrictionTypes as $action ) {
2451
			if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2452
				$expiry[$action] = 'infinity';
2453
			}
2454
			if ( !isset( $limit[$action] ) ) {
2455
				$limit[$action] = '';
2456
			} elseif ( $limit[$action] != '' ) {
2457
				$protect = true;
2458
			}
2459
2460
			// Get current restrictions on $action
2461
			$current = implode( '', $this->mTitle->getRestrictions( $action ) );
2462
			if ( $current != '' ) {
2463
				$isProtected = true;
2464
			}
2465
2466
			if ( $limit[$action] != $current ) {
2467
				$changed = true;
2468
			} elseif ( $limit[$action] != '' ) {
2469
				// Only check expiry change if the action is actually being
2470
				// protected, since expiry does nothing on an not-protected
2471
				// action.
2472
				if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2473
					$changed = true;
2474
				}
2475
			}
2476
		}
2477
2478
		if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2479
			$changed = true;
2480
		}
2481
2482
		// If nothing has changed, do nothing
2483
		if ( !$changed ) {
2484
			return Status::newGood();
2485
		}
2486
2487
		if ( !$protect ) { // No protection at all means unprotection
2488
			$revCommentMsg = 'unprotectedarticle';
2489
			$logAction = 'unprotect';
2490
		} elseif ( $isProtected ) {
2491
			$revCommentMsg = 'modifiedarticleprotection';
2492
			$logAction = 'modify';
2493
		} else {
2494
			$revCommentMsg = 'protectedarticle';
2495
			$logAction = 'protect';
2496
		}
2497
2498
		// Truncate for whole multibyte characters
2499
		$reason = $wgContLang->truncate( $reason, 255 );
2500
2501
		$logRelationsValues = [];
2502
		$logRelationsField = null;
2503
		$logParamsDetails = [];
2504
2505
		// Null revision (used for change tag insertion)
2506
		$nullRevision = null;
2507
2508
		if ( $id ) { // Protection of existing page
2509
			if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
2510
				return Status::newGood();
2511
			}
2512
2513
			// Only certain restrictions can cascade...
2514
			$editrestriction = isset( $limit['edit'] )
2515
				? [ $limit['edit'] ]
2516
				: $this->mTitle->getRestrictions( 'edit' );
2517
			foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2518
				$editrestriction[$key] = 'editprotected'; // backwards compatibility
2519
			}
2520
			foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2521
				$editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2522
			}
2523
2524
			$cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2525
			foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2526
				$cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2527
			}
2528
			foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2529
				$cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2530
			}
2531
2532
			// The schema allows multiple restrictions
2533
			if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2534
				$cascade = false;
2535
			}
2536
2537
			// insert null revision to identify the page protection change as edit summary
2538
			$latest = $this->getLatest();
2539
			$nullRevision = $this->insertProtectNullRevision(
2540
				$revCommentMsg,
2541
				$limit,
2542
				$expiry,
2543
				$cascade,
2544
				$reason,
2545
				$user
2546
			);
2547
2548
			if ( $nullRevision === null ) {
2549
				return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2550
			}
2551
2552
			$logRelationsField = 'pr_id';
2553
2554
			// Update restrictions table
2555
			foreach ( $limit as $action => $restrictions ) {
2556
				$dbw->delete(
2557
					'page_restrictions',
2558
					[
2559
						'pr_page' => $id,
2560
						'pr_type' => $action
2561
					],
2562
					__METHOD__
2563
				);
2564
				if ( $restrictions != '' ) {
2565
					$cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2566
					$dbw->insert(
2567
						'page_restrictions',
2568
						[
2569
							'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2570
							'pr_page' => $id,
2571
							'pr_type' => $action,
2572
							'pr_level' => $restrictions,
2573
							'pr_cascade' => $cascadeValue,
2574
							'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2575
						],
2576
						__METHOD__
2577
					);
2578
					$logRelationsValues[] = $dbw->insertId();
2579
					$logParamsDetails[] = [
2580
						'type' => $action,
2581
						'level' => $restrictions,
2582
						'expiry' => $expiry[$action],
2583
						'cascade' => (bool)$cascadeValue,
2584
					];
2585
				}
2586
			}
2587
2588
			// Clear out legacy restriction fields
2589
			$dbw->update(
2590
				'page',
2591
				[ 'page_restrictions' => '' ],
2592
				[ 'page_id' => $id ],
2593
				__METHOD__
2594
			);
2595
2596
			Hooks::run( 'NewRevisionFromEditComplete',
2597
				[ $this, $nullRevision, $latest, $user ] );
2598
			Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
2599
		} else { // Protection of non-existing page (also known as "title protection")
2600
			// Cascade protection is meaningless in this case
2601
			$cascade = false;
2602
2603
			if ( $limit['create'] != '' ) {
2604
				$dbw->replace( 'protected_titles',
2605
					[ [ 'pt_namespace', 'pt_title' ] ],
2606
					[
2607
						'pt_namespace' => $this->mTitle->getNamespace(),
2608
						'pt_title' => $this->mTitle->getDBkey(),
2609
						'pt_create_perm' => $limit['create'],
2610
						'pt_timestamp' => $dbw->timestamp(),
2611
						'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2612
						'pt_user' => $user->getId(),
2613
						'pt_reason' => $reason,
2614
					], __METHOD__
2615
				);
2616
				$logParamsDetails[] = [
2617
					'type' => 'create',
2618
					'level' => $limit['create'],
2619
					'expiry' => $expiry['create'],
2620
				];
2621
			} else {
2622
				$dbw->delete( 'protected_titles',
2623
					[
2624
						'pt_namespace' => $this->mTitle->getNamespace(),
2625
						'pt_title' => $this->mTitle->getDBkey()
2626
					], __METHOD__
2627
				);
2628
			}
2629
		}
2630
2631
		$this->mTitle->flushRestrictions();
2632
		InfoAction::invalidateCache( $this->mTitle );
2633
2634
		if ( $logAction == 'unprotect' ) {
2635
			$params = [];
2636
		} else {
2637
			$protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2638
			$params = [
2639
				'4::description' => $protectDescriptionLog, // parameter for IRC
2640
				'5:bool:cascade' => $cascade,
2641
				'details' => $logParamsDetails, // parameter for localize and api
2642
			];
2643
		}
2644
2645
		// Update the protection log
2646
		$logEntry = new ManualLogEntry( 'protect', $logAction );
2647
		$logEntry->setTarget( $this->mTitle );
2648
		$logEntry->setComment( $reason );
2649
		$logEntry->setPerformer( $user );
2650
		$logEntry->setParameters( $params );
2651
		if ( !is_null( $nullRevision ) ) {
2652
			$logEntry->setAssociatedRevId( $nullRevision->getId() );
2653
		}
2654
		$logEntry->setTags( $tags );
0 ignored issues
show
Bug introduced by
It seems like $tags defined by parameter $tags on line 2423 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...
2655
		if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2656
			$logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2657
		}
2658
		$logId = $logEntry->insert();
2659
		$logEntry->publish( $logId );
2660
2661
		return Status::newGood( $logId );
2662
	}
2663
2664
	/**
2665
	 * Insert a new null revision for this page.
2666
	 *
2667
	 * @param string $revCommentMsg Comment message key for the revision
2668
	 * @param array $limit Set of restriction keys
2669
	 * @param array $expiry Per restriction type expiration
2670
	 * @param int $cascade Set to false if cascading protection isn't allowed.
2671
	 * @param string $reason
2672
	 * @param User|null $user
2673
	 * @return Revision|null Null on error
2674
	 */
2675
	public function insertProtectNullRevision( $revCommentMsg, array $limit,
2676
		array $expiry, $cascade, $reason, $user = null
2677
	) {
2678
		global $wgContLang;
2679
		$dbw = wfGetDB( DB_MASTER );
2680
2681
		// Prepare a null revision to be added to the history
2682
		$editComment = $wgContLang->ucfirst(
2683
			wfMessage(
2684
				$revCommentMsg,
2685
				$this->mTitle->getPrefixedText()
2686
			)->inContentLanguage()->text()
2687
		);
2688
		if ( $reason ) {
2689
			$editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2690
		}
2691
		$protectDescription = $this->protectDescription( $limit, $expiry );
2692
		if ( $protectDescription ) {
2693
			$editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2694
			$editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2695
				->inContentLanguage()->text();
2696
		}
2697
		if ( $cascade ) {
2698
			$editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2699
			$editComment .= wfMessage( 'brackets' )->params(
2700
				wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2701
			)->inContentLanguage()->text();
2702
		}
2703
2704
		$nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 2679 can be null; however, Revision::newNullRevision() 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...
2705
		if ( $nullRev ) {
2706
			$nullRev->insertOn( $dbw );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 2679 can be null; however, Revision::insertOn() 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...
2707
2708
			// Update page record and touch page
2709
			$oldLatest = $nullRev->getParentId();
2710
			$this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
0 ignored issues
show
Bug introduced by
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 2679 can be null; however, WikiPage::updateRevisionOn() 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...
2711
		}
2712
2713
		return $nullRev;
2714
	}
2715
2716
	/**
2717
	 * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid
2718
	 * @return string
2719
	 */
2720
	protected function formatExpiry( $expiry ) {
2721
		global $wgContLang;
2722
2723
		if ( $expiry != 'infinity' ) {
2724
			return wfMessage(
2725
				'protect-expiring',
2726
				$wgContLang->timeanddate( $expiry, false, false ),
2727
				$wgContLang->date( $expiry, false, false ),
2728
				$wgContLang->time( $expiry, false, false )
2729
			)->inContentLanguage()->text();
2730
		} else {
2731
			return wfMessage( 'protect-expiry-indefinite' )
2732
				->inContentLanguage()->text();
2733
		}
2734
	}
2735
2736
	/**
2737
	 * Builds the description to serve as comment for the edit.
2738
	 *
2739
	 * @param array $limit Set of restriction keys
2740
	 * @param array $expiry Per restriction type expiration
2741
	 * @return string
2742
	 */
2743
	public function protectDescription( array $limit, array $expiry ) {
2744
		$protectDescription = '';
2745
2746
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2747
			# $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2748
			# All possible message keys are listed here for easier grepping:
2749
			# * restriction-create
2750
			# * restriction-edit
2751
			# * restriction-move
2752
			# * restriction-upload
2753
			$actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2754
			# $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2755
			# with '' filtered out. All possible message keys are listed below:
2756
			# * protect-level-autoconfirmed
2757
			# * protect-level-sysop
2758
			$restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2759
				->inContentLanguage()->text();
2760
2761
			$expiryText = $this->formatExpiry( $expiry[$action] );
2762
2763
			if ( $protectDescription !== '' ) {
2764
				$protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2765
			}
2766
			$protectDescription .= wfMessage( 'protect-summary-desc' )
2767
				->params( $actionText, $restrictionsText, $expiryText )
2768
				->inContentLanguage()->text();
2769
		}
2770
2771
		return $protectDescription;
2772
	}
2773
2774
	/**
2775
	 * Builds the description to serve as comment for the log entry.
2776
	 *
2777
	 * Some bots may parse IRC lines, which are generated from log entries which contain plain
2778
	 * protect description text. Keep them in old format to avoid breaking compatibility.
2779
	 * TODO: Fix protection log to store structured description and format it on-the-fly.
2780
	 *
2781
	 * @param array $limit Set of restriction keys
2782
	 * @param array $expiry Per restriction type expiration
2783
	 * @return string
2784
	 */
2785
	public function protectDescriptionLog( array $limit, array $expiry ) {
2786
		global $wgContLang;
2787
2788
		$protectDescriptionLog = '';
2789
2790
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2791
			$expiryText = $this->formatExpiry( $expiry[$action] );
2792
			$protectDescriptionLog .= $wgContLang->getDirMark() .
2793
				"[$action=$restrictions] ($expiryText)";
2794
		}
2795
2796
		return trim( $protectDescriptionLog );
2797
	}
2798
2799
	/**
2800
	 * Take an array of page restrictions and flatten it to a string
2801
	 * suitable for insertion into the page_restrictions field.
2802
	 *
2803
	 * @param string[] $limit
2804
	 *
2805
	 * @throws MWException
2806
	 * @return string
2807
	 */
2808
	protected static function flattenRestrictions( $limit ) {
2809
		if ( !is_array( $limit ) ) {
2810
			throw new MWException( __METHOD__ . ' given non-array restriction set' );
2811
		}
2812
2813
		$bits = [];
2814
		ksort( $limit );
2815
2816
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2817
			$bits[] = "$action=$restrictions";
2818
		}
2819
2820
		return implode( ':', $bits );
2821
	}
2822
2823
	/**
2824
	 * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
2825
	 * backwards compatibility, if you care about error reporting you should use
2826
	 * doDeleteArticleReal() instead.
2827
	 *
2828
	 * Deletes the article with database consistency, writes logs, purges caches
2829
	 *
2830
	 * @param string $reason Delete reason for deletion log
2831
	 * @param bool $suppress Suppress all revisions and log the deletion in
2832
	 *        the suppression log instead of the deletion log
2833
	 * @param int $u1 Unused
2834
	 * @param bool $u2 Unused
2835
	 * @param array|string &$error Array of errors to append to
2836
	 * @param User $user The deleting user
2837
	 * @return bool True if successful
2838
	 */
2839
	public function doDeleteArticle(
2840
		$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2841
	) {
2842
		$status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2843
		return $status->isGood();
2844
	}
2845
2846
	/**
2847
	 * Back-end article deletion
2848
	 * Deletes the article with database consistency, writes logs, purges caches
2849
	 *
2850
	 * @since 1.19
2851
	 *
2852
	 * @param string $reason Delete reason for deletion log
2853
	 * @param bool $suppress Suppress all revisions and log the deletion in
2854
	 *   the suppression log instead of the deletion log
2855
	 * @param int $u1 Unused
2856
	 * @param bool $u2 Unused
2857
	 * @param array|string &$error Array of errors to append to
2858
	 * @param User $user The deleting user
2859
	 * @param array $tags Tags to apply to the deletion action
2860
	 * @return Status Status object; if successful, $status->value is the log_id of the
2861
	 *   deletion log entry. If the page couldn't be deleted because it wasn't
2862
	 *   found, $status is a non-fatal 'cannotdelete' error
2863
	 */
2864
	public function doDeleteArticleReal(
2865
		$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
2866
		$tags = []
2867
	) {
2868
		global $wgUser, $wgContentHandlerUseDB;
2869
2870
		wfDebug( __METHOD__ . "\n" );
2871
2872
		$status = Status::newGood();
2873
2874
		if ( $this->mTitle->getDBkey() === '' ) {
2875
			$status->error( 'cannotdelete',
2876
				wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2877
			return $status;
2878
		}
2879
2880
		$user = is_null( $user ) ? $wgUser : $user;
2881
		if ( !Hooks::run( 'ArticleDelete',
2882
			[ &$this, &$user, &$reason, &$error, &$status, $suppress ]
2883
		) ) {
2884
			if ( $status->isOK() ) {
2885
				// Hook aborted but didn't set a fatal status
2886
				$status->fatal( 'delete-hook-aborted' );
2887
			}
2888
			return $status;
2889
		}
2890
2891
		$dbw = wfGetDB( DB_MASTER );
2892
		$dbw->startAtomic( __METHOD__ );
2893
2894
		$this->loadPageData( self::READ_LATEST );
2895
		$id = $this->getId();
2896
		// T98706: lock the page from various other updates but avoid using
2897
		// WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2898
		// the revisions queries (which also JOIN on user). Only lock the page
2899
		// row and CAS check on page_latest to see if the trx snapshot matches.
2900
		$lockedLatest = $this->lockAndGetLatest();
2901
		if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2902
			$dbw->endAtomic( __METHOD__ );
2903
			// Page not there or trx snapshot is stale
2904
			$status->error( 'cannotdelete',
2905
				wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2906
			return $status;
2907
		}
2908
2909
		// Given the lock above, we can be confident in the title and page ID values
2910
		$namespace = $this->getTitle()->getNamespace();
2911
		$dbKey = $this->getTitle()->getDBkey();
2912
2913
		// At this point we are now comitted to returning an OK
2914
		// status unless some DB query error or other exception comes up.
2915
		// This way callers don't have to call rollback() if $status is bad
2916
		// unless they actually try to catch exceptions (which is rare).
2917
2918
		// we need to remember the old content so we can use it to generate all deletion updates.
2919
		$revision = $this->getRevision();
2920
		try {
2921
			$content = $this->getContent( Revision::RAW );
2922
		} catch ( Exception $ex ) {
2923
			wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2924
				. $ex->getMessage() );
2925
2926
			$content = null;
2927
		}
2928
2929
		$fields = Revision::selectFields();
2930
		$bitfield = false;
2931
2932
		// Bitfields to further suppress the content
2933
		if ( $suppress ) {
2934
			$bitfield = Revision::SUPPRESSED_ALL;
2935
			$fields = array_diff( $fields, [ 'rev_deleted' ] );
2936
		}
2937
2938
		// For now, shunt the revision data into the archive table.
2939
		// Text is *not* removed from the text table; bulk storage
2940
		// is left intact to avoid breaking block-compression or
2941
		// immutable storage schemes.
2942
		// In the future, we may keep revisions and mark them with
2943
		// the rev_deleted field, which is reserved for this purpose.
2944
2945
		// Get all of the page revisions
2946
		$res = $dbw->select(
2947
			'revision',
2948
			$fields,
2949
			[ 'rev_page' => $id ],
2950
			__METHOD__,
2951
			'FOR UPDATE'
2952
		);
2953
		// Build their equivalent archive rows
2954
		$rowsInsert = [];
2955
		foreach ( $res as $row ) {
2956
			$rowInsert = [
2957
				'ar_namespace'  => $namespace,
2958
				'ar_title'      => $dbKey,
2959
				'ar_comment'    => $row->rev_comment,
2960
				'ar_user'       => $row->rev_user,
2961
				'ar_user_text'  => $row->rev_user_text,
2962
				'ar_timestamp'  => $row->rev_timestamp,
2963
				'ar_minor_edit' => $row->rev_minor_edit,
2964
				'ar_rev_id'     => $row->rev_id,
2965
				'ar_parent_id'  => $row->rev_parent_id,
2966
				'ar_text_id'    => $row->rev_text_id,
2967
				'ar_text'       => '',
2968
				'ar_flags'      => '',
2969
				'ar_len'        => $row->rev_len,
2970
				'ar_page_id'    => $id,
2971
				'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
2972
				'ar_sha1'       => $row->rev_sha1,
2973
			];
2974
			if ( $wgContentHandlerUseDB ) {
2975
				$rowInsert['ar_content_model'] = $row->rev_content_model;
2976
				$rowInsert['ar_content_format'] = $row->rev_content_format;
2977
			}
2978
			$rowsInsert[] = $rowInsert;
2979
		}
2980
		// Copy them into the archive table
2981
		$dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2982
		// Save this so we can pass it to the ArticleDeleteComplete hook.
2983
		$archivedRevisionCount = $dbw->affectedRows();
2984
2985
		// Clone the title and wikiPage, so we have the information we need when
2986
		// we log and run the ArticleDeleteComplete hook.
2987
		$logTitle = clone $this->mTitle;
2988
		$wikiPageBeforeDelete = clone $this;
2989
2990
		// Now that it's safely backed up, delete it
2991
		$dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2992
		$dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2993
2994
		// Log the deletion, if the page was suppressed, put it in the suppression log instead
2995
		$logtype = $suppress ? 'suppress' : 'delete';
2996
2997
		$logEntry = new ManualLogEntry( $logtype, 'delete' );
2998
		$logEntry->setPerformer( $user );
2999
		$logEntry->setTarget( $logTitle );
3000
		$logEntry->setComment( $reason );
3001
		$logEntry->setTags( $tags );
3002
		$logid = $logEntry->insert();
3003
3004
		$dbw->onTransactionPreCommitOrIdle(
3005
			function () use ( $dbw, $logEntry, $logid ) {
3006
				// Bug 56776: avoid deadlocks (especially from FileDeleteForm)
3007
				$logEntry->publish( $logid );
3008
			},
3009
			__METHOD__
3010
		);
3011
3012
		$dbw->endAtomic( __METHOD__ );
3013
3014
		$this->doDeleteUpdates( $id, $content, $revision );
3015
3016
		Hooks::run( 'ArticleDeleteComplete', [
3017
			&$wikiPageBeforeDelete,
3018
			&$user,
3019
			$reason,
3020
			$id,
3021
			$content,
3022
			$logEntry,
3023
			$archivedRevisionCount
3024
		] );
3025
		$status->value = $logid;
3026
3027
		// Show log excerpt on 404 pages rather than just a link
3028
		$cache = ObjectCache::getMainStashInstance();
3029
		$key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
3030
		$cache->set( $key, 1, $cache::TTL_DAY );
3031
3032
		return $status;
3033
	}
3034
3035
	/**
3036
	 * Lock the page row for this title+id and return page_latest (or 0)
3037
	 *
3038
	 * @return integer Returns 0 if no row was found with this title+id
3039
	 * @since 1.27
3040
	 */
3041
	public function lockAndGetLatest() {
3042
		return (int)wfGetDB( DB_MASTER )->selectField(
3043
			'page',
3044
			'page_latest',
3045
			[
3046
				'page_id' => $this->getId(),
3047
				// Typically page_id is enough, but some code might try to do
3048
				// updates assuming the title is the same, so verify that
3049
				'page_namespace' => $this->getTitle()->getNamespace(),
3050
				'page_title' => $this->getTitle()->getDBkey()
3051
			],
3052
			__METHOD__,
3053
			[ 'FOR UPDATE' ]
3054
		);
3055
	}
3056
3057
	/**
3058
	 * Do some database updates after deletion
3059
	 *
3060
	 * @param int $id The page_id value of the page being deleted
3061
	 * @param Content|null $content Optional page content to be used when determining
3062
	 *   the required updates. This may be needed because $this->getContent()
3063
	 *   may already return null when the page proper was deleted.
3064
	 * @param Revision|null $revision The latest page revision
3065
	 */
3066
	public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
3067
		try {
3068
			$countable = $this->isCountable();
3069
		} catch ( Exception $ex ) {
3070
			// fallback for deleting broken pages for which we cannot load the content for
3071
			// some reason. Note that doDeleteArticleReal() already logged this problem.
3072
			$countable = false;
3073
		}
3074
3075
		// Update site status
3076
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
3077
3078
		// Delete pagelinks, update secondary indexes, etc
3079
		$updates = $this->getDeletionUpdates( $content );
3080
		foreach ( $updates as $update ) {
3081
			DeferredUpdates::addUpdate( $update );
3082
		}
3083
3084
		// Reparse any pages transcluding this page
3085
		LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
3086
3087
		// Reparse any pages including this image
3088
		if ( $this->mTitle->getNamespace() == NS_FILE ) {
3089
			LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
3090
		}
3091
3092
		// Clear caches
3093
		WikiPage::onArticleDelete( $this->mTitle );
3094
		ResourceLoaderWikiModule::invalidateModuleCache(
3095
			$this->mTitle, $revision, null, wfWikiID()
3096
		);
3097
3098
		// Reset this object and the Title object
3099
		$this->loadFromRow( false, self::READ_LATEST );
3100
3101
		// Search engine
3102
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3103
	}
3104
3105
	/**
3106
	 * Roll back the most recent consecutive set of edits to a page
3107
	 * from the same user; fails if there are no eligible edits to
3108
	 * roll back to, e.g. user is the sole contributor. This function
3109
	 * performs permissions checks on $user, then calls commitRollback()
3110
	 * to do the dirty work
3111
	 *
3112
	 * @todo Separate the business/permission stuff out from backend code
3113
	 * @todo Remove $token parameter. Already verified by RollbackAction and ApiRollback.
3114
	 *
3115
	 * @param string $fromP Name of the user whose edits to rollback.
3116
	 * @param string $summary Custom summary. Set to default summary if empty.
3117
	 * @param string $token Rollback token.
3118
	 * @param bool $bot If true, mark all reverted edits as bot.
3119
	 *
3120
	 * @param array $resultDetails Array contains result-specific array of additional values
3121
	 *    'alreadyrolled' : 'current' (rev)
3122
	 *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
3123
	 *
3124
	 * @param User $user The user performing the rollback
3125
	 * @param array|null $tags Change tags to apply to the rollback
3126
	 * Callers are responsible for permission checks
3127
	 * (with ChangeTags::canAddTagsAccompanyingChange)
3128
	 *
3129
	 * @return array Array of errors, each error formatted as
3130
	 *   array(messagekey, param1, param2, ...).
3131
	 * On success, the array is empty.  This array can also be passed to
3132
	 * OutputPage::showPermissionsErrorPage().
3133
	 */
3134
	public function doRollback(
3135
		$fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3136
	) {
3137
		$resultDetails = null;
3138
3139
		// Check permissions
3140
		$editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3141
		$rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3142
		$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3143
3144
		if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3145
			$errors[] = [ 'sessionfailure' ];
3146
		}
3147
3148
		if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3149
			$errors[] = [ 'actionthrottledtext' ];
3150
		}
3151
3152
		// If there were errors, bail out now
3153
		if ( !empty( $errors ) ) {
3154
			return $errors;
3155
		}
3156
3157
		return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3158
	}
3159
3160
	/**
3161
	 * Backend implementation of doRollback(), please refer there for parameter
3162
	 * and return value documentation
3163
	 *
3164
	 * NOTE: This function does NOT check ANY permissions, it just commits the
3165
	 * rollback to the DB. Therefore, you should only call this function direct-
3166
	 * ly if you want to use custom permissions checks. If you don't, use
3167
	 * doRollback() instead.
3168
	 * @param string $fromP Name of the user whose edits to rollback.
3169
	 * @param string $summary Custom summary. Set to default summary if empty.
3170
	 * @param bool $bot If true, mark all reverted edits as bot.
3171
	 *
3172
	 * @param array $resultDetails Contains result-specific array of additional values
3173
	 * @param User $guser The user performing the rollback
3174
	 * @param array|null $tags Change tags to apply to the rollback
3175
	 * Callers are responsible for permission checks
3176
	 * (with ChangeTags::canAddTagsAccompanyingChange)
3177
	 *
3178
	 * @return array
3179
	 */
3180
	public function commitRollback( $fromP, $summary, $bot,
3181
		&$resultDetails, User $guser, $tags = null
3182
	) {
3183
		global $wgUseRCPatrol, $wgContLang;
3184
3185
		$dbw = wfGetDB( DB_MASTER );
3186
3187
		if ( wfReadOnly() ) {
3188
			return [ [ 'readonlytext' ] ];
3189
		}
3190
3191
		// Get the last editor
3192
		$current = $this->getRevision();
3193
		if ( is_null( $current ) ) {
3194
			// Something wrong... no page?
3195
			return [ [ 'notanarticle' ] ];
3196
		}
3197
3198
		$from = str_replace( '_', ' ', $fromP );
3199
		// User name given should match up with the top revision.
3200
		// If the user was deleted then $from should be empty.
3201 View Code Duplication
		if ( $from != $current->getUserText() ) {
3202
			$resultDetails = [ 'current' => $current ];
3203
			return [ [ 'alreadyrolled',
3204
				htmlspecialchars( $this->mTitle->getPrefixedText() ),
3205
				htmlspecialchars( $fromP ),
3206
				htmlspecialchars( $current->getUserText() )
3207
			] ];
3208
		}
3209
3210
		// Get the last edit not by this person...
3211
		// Note: these may not be public values
3212
		$user = intval( $current->getUser( Revision::RAW ) );
3213
		$user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
3214
		$s = $dbw->selectRow( 'revision',
3215
			[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3216
			[ 'rev_page' => $current->getPage(),
3217
				"rev_user != {$user} OR rev_user_text != {$user_text}"
3218
			], __METHOD__,
3219
			[ 'USE INDEX' => 'page_timestamp',
3220
				'ORDER BY' => 'rev_timestamp DESC' ]
3221
			);
3222
		if ( $s === false ) {
3223
			// No one else ever edited this page
3224
			return [ [ 'cantrollback' ] ];
3225
		} elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3226
			|| $s->rev_deleted & Revision::DELETED_USER
3227
		) {
3228
			// Only admins can see this text
3229
			return [ [ 'notvisiblerev' ] ];
3230
		}
3231
3232
		// Generate the edit summary if necessary
3233
		$target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3234
		if ( empty( $summary ) ) {
3235
			if ( $from == '' ) { // no public user name
3236
				$summary = wfMessage( 'revertpage-nouser' );
3237
			} else {
3238
				$summary = wfMessage( 'revertpage' );
3239
			}
3240
		}
3241
3242
		// Allow the custom summary to use the same args as the default message
3243
		$args = [
3244
			$target->getUserText(), $from, $s->rev_id,
3245
			$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3246
			$current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3247
		];
3248
		if ( $summary instanceof Message ) {
3249
			$summary = $summary->params( $args )->inContentLanguage()->text();
3250
		} else {
3251
			$summary = wfMsgReplaceArgs( $summary, $args );
3252
		}
3253
3254
		// Trim spaces on user supplied text
3255
		$summary = trim( $summary );
3256
3257
		// Truncate for whole multibyte characters.
3258
		$summary = $wgContLang->truncate( $summary, 255 );
3259
3260
		// Save
3261
		$flags = EDIT_UPDATE | EDIT_INTERNAL;
3262
3263
		if ( $guser->isAllowed( 'minoredit' ) ) {
3264
			$flags |= EDIT_MINOR;
3265
		}
3266
3267
		if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3268
			$flags |= EDIT_FORCE_BOT;
3269
		}
3270
3271
		$targetContent = $target->getContent();
3272
		$changingContentModel = $targetContent->getModel() !== $current->getContentModel();
3273
3274
		// Actually store the edit
3275
		$status = $this->doEditContent(
3276
			$targetContent,
3277
			$summary,
3278
			$flags,
3279
			$target->getId(),
3280
			$guser,
3281
			null,
3282
			$tags
3283
		);
3284
3285
		// Set patrolling and bot flag on the edits, which gets rollbacked.
3286
		// This is done even on edit failure to have patrolling in that case (bug 62157).
3287
		$set = [];
3288
		if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3289
			// Mark all reverted edits as bot
3290
			$set['rc_bot'] = 1;
3291
		}
3292
3293
		if ( $wgUseRCPatrol ) {
3294
			// Mark all reverted edits as patrolled
3295
			$set['rc_patrolled'] = 1;
3296
		}
3297
3298
		if ( count( $set ) ) {
3299
			$dbw->update( 'recentchanges', $set,
3300
				[ /* WHERE */
3301
					'rc_cur_id' => $current->getPage(),
3302
					'rc_user_text' => $current->getUserText(),
3303
					'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3304
				],
3305
				__METHOD__
3306
			);
3307
		}
3308
3309
		if ( !$status->isOK() ) {
3310
			return $status->getErrorsArray();
3311
		}
3312
3313
		// raise error, when the edit is an edit without a new version
3314
		$statusRev = isset( $status->value['revision'] )
3315
			? $status->value['revision']
3316
			: null;
3317 View Code Duplication
		if ( !( $statusRev instanceof Revision ) ) {
3318
			$resultDetails = [ 'current' => $current ];
3319
			return [ [ 'alreadyrolled',
3320
					htmlspecialchars( $this->mTitle->getPrefixedText() ),
3321
					htmlspecialchars( $fromP ),
3322
					htmlspecialchars( $current->getUserText() )
3323
			] ];
3324
		}
3325
3326
		if ( $changingContentModel ) {
3327
			// If the content model changed during the rollback,
3328
			// make sure it gets logged to Special:Log/contentmodel
3329
			$log = new ManualLogEntry( 'contentmodel', 'change' );
3330
			$log->setPerformer( $guser );
3331
			$log->setTarget( $this->mTitle );
3332
			$log->setComment( $summary );
3333
			$log->setParameters( [
3334
				'4::oldmodel' => $current->getContentModel(),
3335
				'5::newmodel' => $targetContent->getModel(),
3336
			] );
3337
3338
			$logId = $log->insert( $dbw );
3339
			$log->publish( $logId );
3340
		}
3341
3342
		$revId = $statusRev->getId();
3343
3344
		Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3345
3346
		$resultDetails = [
3347
			'summary' => $summary,
3348
			'current' => $current,
3349
			'target' => $target,
3350
			'newid' => $revId
3351
		];
3352
3353
		return [];
3354
	}
3355
3356
	/**
3357
	 * The onArticle*() functions are supposed to be a kind of hooks
3358
	 * which should be called whenever any of the specified actions
3359
	 * are done.
3360
	 *
3361
	 * This is a good place to put code to clear caches, for instance.
3362
	 *
3363
	 * This is called on page move and undelete, as well as edit
3364
	 *
3365
	 * @param Title $title
3366
	 */
3367
	public static function onArticleCreate( Title $title ) {
3368
		// Update existence markers on article/talk tabs...
3369
		$other = $title->getOtherPage();
3370
3371
		$other->purgeSquid();
3372
3373
		$title->touchLinks();
3374
		$title->purgeSquid();
3375
		$title->deleteTitleProtection();
3376
3377
		MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3378
3379
		if ( $title->getNamespace() == NS_CATEGORY ) {
3380
			// Load the Category object, which will schedule a job to create
3381
			// the category table row if necessary. Checking a replica DB is ok
3382
			// here, in the worst case it'll run an unnecessary recount job on
3383
			// a category that probably doesn't have many members.
3384
			Category::newFromTitle( $title )->getID();
3385
		}
3386
	}
3387
3388
	/**
3389
	 * Clears caches when article is deleted
3390
	 *
3391
	 * @param Title $title
3392
	 */
3393
	public static function onArticleDelete( Title $title ) {
3394
		global $wgContLang;
3395
3396
		// Update existence markers on article/talk tabs...
3397
		$other = $title->getOtherPage();
3398
3399
		$other->purgeSquid();
3400
3401
		$title->touchLinks();
3402
		$title->purgeSquid();
3403
3404
		MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3405
3406
		// File cache
3407
		HTMLFileCache::clearFileCache( $title );
3408
		InfoAction::invalidateCache( $title );
3409
3410
		// Messages
3411
		if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3412
			MessageCache::singleton()->replace( $title->getDBkey(), false );
3413
3414
			if ( $wgContLang->hasVariants() ) {
3415
				$wgContLang->updateConversionTable( $title );
3416
			}
3417
		}
3418
3419
		// Images
3420
		if ( $title->getNamespace() == NS_FILE ) {
3421
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3422
		}
3423
3424
		// User talk pages
3425
		if ( $title->getNamespace() == NS_USER_TALK ) {
3426
			$user = User::newFromName( $title->getText(), false );
3427
			if ( $user ) {
3428
				$user->setNewtalk( false );
3429
			}
3430
		}
3431
3432
		// Image redirects
3433
		RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3434
	}
3435
3436
	/**
3437
	 * Purge caches on page update etc
3438
	 *
3439
	 * @param Title $title
3440
	 * @param Revision|null $revision Revision that was just saved, may be null
3441
	 */
3442
	public static function onArticleEdit( Title $title, Revision $revision = null ) {
3443
		// Invalidate caches of articles which include this page
3444
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3445
3446
		// Invalidate the caches of all pages which redirect here
3447
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3448
3449
		MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
3450
3451
		// Purge CDN for this page only
3452
		$title->purgeSquid();
3453
		// Clear file cache for this page only
3454
		HTMLFileCache::clearFileCache( $title );
3455
3456
		$revid = $revision ? $revision->getId() : null;
3457
		DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
3458
			InfoAction::invalidateCache( $title, $revid );
3459
		} );
3460
	}
3461
3462
	/**#@-*/
3463
3464
	/**
3465
	 * Returns a list of categories this page is a member of.
3466
	 * Results will include hidden categories
3467
	 *
3468
	 * @return TitleArray
3469
	 */
3470
	public function getCategories() {
3471
		$id = $this->getId();
3472
		if ( $id == 0 ) {
3473
			return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3474
		}
3475
3476
		$dbr = wfGetDB( DB_REPLICA );
3477
		$res = $dbr->select( 'categorylinks',
3478
			[ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3479
			// Have to do that since Database::fieldNamesWithAlias treats numeric indexes
3480
			// as not being aliases, and NS_CATEGORY is numeric
3481
			[ 'cl_from' => $id ],
3482
			__METHOD__ );
3483
3484
		return TitleArray::newFromResult( $res );
3485
	}
3486
3487
	/**
3488
	 * Returns a list of hidden categories this page is a member of.
3489
	 * Uses the page_props and categorylinks tables.
3490
	 *
3491
	 * @return array Array of Title objects
3492
	 */
3493
	public function getHiddenCategories() {
3494
		$result = [];
3495
		$id = $this->getId();
3496
3497
		if ( $id == 0 ) {
3498
			return [];
3499
		}
3500
3501
		$dbr = wfGetDB( DB_REPLICA );
3502
		$res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3503
			[ 'cl_to' ],
3504
			[ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3505
				'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3506
			__METHOD__ );
3507
3508
		if ( $res !== false ) {
3509
			foreach ( $res as $row ) {
3510
				$result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3511
			}
3512
		}
3513
3514
		return $result;
3515
	}
3516
3517
	/**
3518
	 * Return an applicable autosummary if one exists for the given edit.
3519
	 * @param string|null $oldtext The previous text of the page.
3520
	 * @param string|null $newtext The submitted text of the page.
3521
	 * @param int $flags Bitmask: a bitmask of flags submitted for the edit.
3522
	 * @return string An appropriate autosummary, or an empty string.
3523
	 *
3524
	 * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
3525
	 */
3526
	public static function getAutosummary( $oldtext, $newtext, $flags ) {
3527
		// NOTE: stub for backwards-compatibility. assumes the given text is
3528
		// wikitext. will break horribly if it isn't.
3529
3530
		wfDeprecated( __METHOD__, '1.21' );
3531
3532
		$handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
3533
		$oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3534
		$newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3535
3536
		return $handler->getAutosummary( $oldContent, $newContent, $flags );
3537
	}
3538
3539
	/**
3540
	 * Auto-generates a deletion reason
3541
	 *
3542
	 * @param bool &$hasHistory Whether the page has a history
3543
	 * @return string|bool String containing deletion reason or empty string, or boolean false
3544
	 *    if no revision occurred
3545
	 */
3546
	public function getAutoDeleteReason( &$hasHistory ) {
3547
		return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3548
	}
3549
3550
	/**
3551
	 * Update all the appropriate counts in the category table, given that
3552
	 * we've added the categories $added and deleted the categories $deleted.
3553
	 *
3554
	 * This should only be called from deferred updates or jobs to avoid contention.
3555
	 *
3556
	 * @param array $added The names of categories that were added
3557
	 * @param array $deleted The names of categories that were deleted
3558
	 * @param integer $id Page ID (this should be the original deleted page ID)
3559
	 */
3560
	public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3561
		$id = $id ?: $this->getId();
3562
		$ns = $this->getTitle()->getNamespace();
3563
3564
		$addFields = [ 'cat_pages = cat_pages + 1' ];
3565
		$removeFields = [ 'cat_pages = cat_pages - 1' ];
3566
		if ( $ns == NS_CATEGORY ) {
3567
			$addFields[] = 'cat_subcats = cat_subcats + 1';
3568
			$removeFields[] = 'cat_subcats = cat_subcats - 1';
3569
		} elseif ( $ns == NS_FILE ) {
3570
			$addFields[] = 'cat_files = cat_files + 1';
3571
			$removeFields[] = 'cat_files = cat_files - 1';
3572
		}
3573
3574
		$dbw = wfGetDB( DB_MASTER );
3575
3576
		if ( count( $added ) ) {
3577
			$existingAdded = $dbw->selectFieldValues(
3578
				'category',
3579
				'cat_title',
3580
				[ 'cat_title' => $added ],
3581
				__METHOD__
3582
			);
3583
3584
			// For category rows that already exist, do a plain
3585
			// UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3586
			// to avoid creating gaps in the cat_id sequence.
3587
			if ( count( $existingAdded ) ) {
3588
				$dbw->update(
3589
					'category',
3590
					$addFields,
3591
					[ 'cat_title' => $existingAdded ],
3592
					__METHOD__
3593
				);
3594
			}
3595
3596
			$missingAdded = array_diff( $added, $existingAdded );
3597
			if ( count( $missingAdded ) ) {
3598
				$insertRows = [];
3599
				foreach ( $missingAdded as $cat ) {
3600
					$insertRows[] = [
3601
						'cat_title'   => $cat,
3602
						'cat_pages'   => 1,
3603
						'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3604
						'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
3605
					];
3606
				}
3607
				$dbw->upsert(
3608
					'category',
3609
					$insertRows,
3610
					[ 'cat_title' ],
3611
					$addFields,
3612
					__METHOD__
3613
				);
3614
			}
3615
		}
3616
3617
		if ( count( $deleted ) ) {
3618
			$dbw->update(
3619
				'category',
3620
				$removeFields,
3621
				[ 'cat_title' => $deleted ],
3622
				__METHOD__
3623
			);
3624
		}
3625
3626
		foreach ( $added as $catName ) {
3627
			$cat = Category::newFromName( $catName );
3628
			Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3629
		}
3630
3631
		foreach ( $deleted as $catName ) {
3632
			$cat = Category::newFromName( $catName );
3633
			Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3634
		}
3635
3636
		// Refresh counts on categories that should be empty now, to
3637
		// trigger possible deletion. Check master for the most
3638
		// up-to-date cat_pages.
3639
		if ( count( $deleted ) ) {
3640
			$rows = $dbw->select(
3641
				'category',
3642
				[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
3643
				[ 'cat_title' => $deleted, 'cat_pages <= 0' ],
3644
				__METHOD__
3645
			);
3646
			foreach ( $rows as $row ) {
3647
				$cat = Category::newFromRow( $row );
3648
				$cat->refreshCounts();
3649
			}
3650
		}
3651
	}
3652
3653
	/**
3654
	 * Opportunistically enqueue link update jobs given fresh parser output if useful
3655
	 *
3656
	 * @param ParserOutput $parserOutput Current version page output
3657
	 * @since 1.25
3658
	 */
3659
	public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3660
		if ( wfReadOnly() ) {
3661
			return;
3662
		}
3663
3664
		if ( !Hooks::run( 'OpportunisticLinksUpdate',
3665
			[ $this, $this->mTitle, $parserOutput ]
3666
		) ) {
3667
			return;
3668
		}
3669
3670
		$config = RequestContext::getMain()->getConfig();
3671
3672
		$params = [
3673
			'isOpportunistic' => true,
3674
			'rootJobTimestamp' => $parserOutput->getCacheTime()
3675
		];
3676
3677
		if ( $this->mTitle->areRestrictionsCascading() ) {
3678
			// If the page is cascade protecting, the links should really be up-to-date
3679
			JobQueueGroup::singleton()->lazyPush(
3680
				RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3681
			);
3682
		} elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3683
			// Assume the output contains "dynamic" time/random based magic words.
3684
			// Only update pages that expired due to dynamic content and NOT due to edits
3685
			// to referenced templates/files. When the cache expires due to dynamic content,
3686
			// page_touched is unchanged. We want to avoid triggering redundant jobs due to
3687
			// views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3688
			// template/file edit already triggered recursive RefreshLinksJob jobs.
3689
			if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3690
				// If a page is uncacheable, do not keep spamming a job for it.
3691
				// Although it would be de-duplicated, it would still waste I/O.
3692
				$cache = ObjectCache::getLocalClusterInstance();
3693
				$key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3694
				$ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3695
				if ( $cache->add( $key, time(), $ttl ) ) {
3696
					JobQueueGroup::singleton()->lazyPush(
3697
						RefreshLinksJob::newDynamic( $this->mTitle, $params )
3698
					);
3699
				}
3700
			}
3701
		}
3702
	}
3703
3704
	/**
3705
	 * Returns a list of updates to be performed when this page is deleted. The
3706
	 * updates should remove any information about this page from secondary data
3707
	 * stores such as links tables.
3708
	 *
3709
	 * @param Content|null $content Optional Content object for determining the
3710
	 *   necessary updates.
3711
	 * @return DeferrableUpdate[]
3712
	 */
3713
	public function getDeletionUpdates( Content $content = null ) {
3714
		if ( !$content ) {
3715
			// load content object, which may be used to determine the necessary updates.
3716
			// XXX: the content may not be needed to determine the updates.
3717
			try {
3718
				$content = $this->getContent( Revision::RAW );
3719
			} catch ( Exception $ex ) {
3720
				// If we can't load the content, something is wrong. Perhaps that's why
3721
				// the user is trying to delete the page, so let's not fail in that case.
3722
				// Note that doDeleteArticleReal() will already have logged an issue with
3723
				// loading the content.
3724
			}
3725
		}
3726
3727
		if ( !$content ) {
3728
			$updates = [];
3729
		} else {
3730
			$updates = $content->getDeletionUpdates( $this );
3731
		}
3732
3733
		Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3734
		return $updates;
3735
	}
3736
3737
	/**
3738
	 * Whether this content displayed on this page
3739
	 * comes from the local database
3740
	 *
3741
	 * @since 1.28
3742
	 * @return bool
3743
	 */
3744
	public function isLocal() {
3745
		return true;
3746
	}
3747
}
3748