Completed
Branch master (a9d73a)
by
unknown
30:07
created

WikiPage::setLastEdit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

    return array();
}

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

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

Loading history...
Bug introduced by
It seems like $from defined by self::convertSelectType($from) on line 148 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...
156
	}
157
158
	/**
159
	 * Constructor from a database row
160
	 *
161
	 * @since 1.20
162
	 * @param object $row Database row containing at least fields returned by selectFields().
163
	 * @param string|int $from Source of $data:
164
	 *        - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
165
	 *        - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
166
	 *        - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
167
	 * @return WikiPage
168
	 */
169
	public static function newFromRow( $row, $from = 'fromdb' ) {
170
		$page = self::factory( Title::newFromRow( $row ) );
171
		$page->loadFromRow( $row, $from );
172
		return $page;
173
	}
174
175
	/**
176
	 * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
177
	 *
178
	 * @param object|string|int $type
179
	 * @return mixed
180
	 */
181
	private static function convertSelectType( $type ) {
182
		switch ( $type ) {
183
		case 'fromdb':
184
			return self::READ_NORMAL;
185
		case 'fromdbmaster':
186
			return self::READ_LATEST;
187
		case 'forupdate':
188
			return self::READ_LOCKING;
189
		default:
190
			// It may already be an integer or whatever else
191
			return $type;
192
		}
193
	}
194
195
	/**
196
	 * @todo Move this UI stuff somewhere else
197
	 *
198
	 * @see ContentHandler::getActionOverrides
199
	 */
200
	public function getActionOverrides() {
201
		return $this->getContentHandler()->getActionOverrides();
202
	}
203
204
	/**
205
	 * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
206
	 *
207
	 * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
208
	 *
209
	 * @return ContentHandler
210
	 *
211
	 * @since 1.21
212
	 */
213
	public function getContentHandler() {
214
		return ContentHandler::getForModelID( $this->getContentModel() );
215
	}
216
217
	/**
218
	 * Get the title object of the article
219
	 * @return Title Title object of this page
220
	 */
221
	public function getTitle() {
222
		return $this->mTitle;
223
	}
224
225
	/**
226
	 * Clear the object
227
	 * @return void
228
	 */
229
	public function clear() {
230
		$this->mDataLoaded = false;
231
		$this->mDataLoadedFrom = self::READ_NONE;
232
233
		$this->clearCacheFields();
234
	}
235
236
	/**
237
	 * Clear the object cache fields
238
	 * @return void
239
	 */
240
	protected function clearCacheFields() {
241
		$this->mId = null;
242
		$this->mRedirectTarget = null; // Title object if set
243
		$this->mLastRevision = null; // Latest revision
244
		$this->mTouched = '19700101000000';
245
		$this->mLinksUpdated = '19700101000000';
246
		$this->mTimestamp = '';
247
		$this->mIsRedirect = false;
248
		$this->mLatest = false;
249
		// Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
250
		// the requested rev ID and content against the cached one for equality. For most
251
		// content types, the output should not change during the lifetime of this cache.
252
		// Clearing it can cause extra parses on edit for no reason.
253
	}
254
255
	/**
256
	 * Clear the mPreparedEdit cache field, as may be needed by mutable content types
257
	 * @return void
258
	 * @since 1.23
259
	 */
260
	public function clearPreparedEdit() {
261
		$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...
262
	}
263
264
	/**
265
	 * Return the list of revision fields that should be selected to create
266
	 * a new page.
267
	 *
268
	 * @return array
269
	 */
270
	public static function selectFields() {
271
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
272
273
		$fields = [
274
			'page_id',
275
			'page_namespace',
276
			'page_title',
277
			'page_restrictions',
278
			'page_is_redirect',
279
			'page_is_new',
280
			'page_random',
281
			'page_touched',
282
			'page_links_updated',
283
			'page_latest',
284
			'page_len',
285
		];
286
287
		if ( $wgContentHandlerUseDB ) {
288
			$fields[] = 'page_content_model';
289
		}
290
291
		if ( $wgPageLanguageUseDB ) {
292
			$fields[] = 'page_lang';
293
		}
294
295
		return $fields;
296
	}
297
298
	/**
299
	 * Fetch a page record with the given conditions
300
	 * @param IDatabase $dbr
301
	 * @param array $conditions
302
	 * @param array $options
303
	 * @return object|bool Database result resource, or false on failure
304
	 */
305
	protected function pageData( $dbr, $conditions, $options = [] ) {
306
		$fields = self::selectFields();
307
308
		Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
309
310
		$row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
311
312
		Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
313
314
		return $row;
315
	}
316
317
	/**
318
	 * Fetch a page record matching the Title object's namespace and title
319
	 * using a sanitized title string
320
	 *
321
	 * @param IDatabase $dbr
322
	 * @param Title $title
323
	 * @param array $options
324
	 * @return object|bool Database result resource, or false on failure
325
	 */
326
	public function pageDataFromTitle( $dbr, $title, $options = [] ) {
327
		return $this->pageData( $dbr, [
328
			'page_namespace' => $title->getNamespace(),
329
			'page_title' => $title->getDBkey() ], $options );
330
	}
331
332
	/**
333
	 * Fetch a page record matching the requested ID
334
	 *
335
	 * @param IDatabase $dbr
336
	 * @param int $id
337
	 * @param array $options
338
	 * @return object|bool Database result resource, or false on failure
339
	 */
340
	public function pageDataFromId( $dbr, $id, $options = [] ) {
341
		return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
342
	}
343
344
	/**
345
	 * Load the object from a given source by title
346
	 *
347
	 * @param object|string|int $from One of the following:
348
	 *   - A DB query result object.
349
	 *   - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB.
350
	 *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
351
	 *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
352
	 *     using SELECT FOR UPDATE.
353
	 *
354
	 * @return void
355
	 */
356
	public function loadPageData( $from = 'fromdb' ) {
357
		$from = self::convertSelectType( $from );
358
		if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
359
			// We already have the data from the correct location, no need to load it twice.
360
			return;
361
		}
362
363
		if ( is_int( $from ) ) {
364
			list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
365
			$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
366
367
			if ( !$data
368
				&& $index == DB_SLAVE
369
				&& 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...
370
				&& 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...
371
			) {
372
				$from = self::READ_LATEST;
373
				list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
374
				$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
375
			}
376
		} else {
377
			// No idea from where the caller got this data, assume slave database.
378
			$data = $from;
379
			$from = self::READ_NORMAL;
380
		}
381
382
		$this->loadFromRow( $data, $from );
0 ignored issues
show
Bug introduced by
It seems like $data defined by $from on line 378 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...
383
	}
384
385
	/**
386
	 * Load the object from a database row
387
	 *
388
	 * @since 1.20
389
	 * @param object|bool $data DB row containing fields returned by selectFields() or false
390
	 * @param string|int $from One of the following:
391
	 *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
392
	 *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
393
	 *        - "forupdate"  or WikiPage::READ_LOCKING if the data comes from
394
	 *          the master DB using SELECT FOR UPDATE
395
	 */
396
	public function loadFromRow( $data, $from ) {
397
		$lc = LinkCache::singleton();
398
		$lc->clearLink( $this->mTitle );
399
400
		if ( $data ) {
401
			$lc->addGoodLinkObjFromRow( $this->mTitle, $data );
402
403
			$this->mTitle->loadFromRow( $data );
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 396 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...
404
405
			// Old-fashioned restrictions
406
			$this->mTitle->loadRestrictions( $data->page_restrictions );
407
408
			$this->mId = intval( $data->page_id );
409
			$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...
410
			$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...
411
			$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...
412
			$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...
413
			// Bug 37225: $latest may no longer match the cached latest Revision object.
414
			// Double-check the ID of any cached latest Revision object for consistency.
415
			if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
416
				$this->mLastRevision = null;
417
				$this->mTimestamp = '';
418
			}
419
		} else {
420
			$lc->addBadLinkObj( $this->mTitle );
421
422
			$this->mTitle->loadFromRow( false );
423
424
			$this->clearCacheFields();
425
426
			$this->mId = 0;
427
		}
428
429
		$this->mDataLoaded = true;
430
		$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...
431
	}
432
433
	/**
434
	 * @return int Page ID
435
	 */
436
	public function getId() {
437
		if ( !$this->mDataLoaded ) {
438
			$this->loadPageData();
439
		}
440
		return $this->mId;
441
	}
442
443
	/**
444
	 * @return bool Whether or not the page exists in the database
445
	 */
446
	public function exists() {
447
		if ( !$this->mDataLoaded ) {
448
			$this->loadPageData();
449
		}
450
		return $this->mId > 0;
451
	}
452
453
	/**
454
	 * Check if this page is something we're going to be showing
455
	 * some sort of sensible content for. If we return false, page
456
	 * views (plain action=view) will return an HTTP 404 response,
457
	 * so spiders and robots can know they're following a bad link.
458
	 *
459
	 * @return bool
460
	 */
461
	public function hasViewableContent() {
462
		return $this->exists() || $this->mTitle->isAlwaysKnown();
463
	}
464
465
	/**
466
	 * Tests if the article content represents a redirect
467
	 *
468
	 * @return bool
469
	 */
470
	public function isRedirect() {
471
		if ( !$this->mDataLoaded ) {
472
			$this->loadPageData();
473
		}
474
475
		return (bool)$this->mIsRedirect;
476
	}
477
478
	/**
479
	 * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
480
	 *
481
	 * Will use the revisions actual content model if the page exists,
482
	 * and the page's default if the page doesn't exist yet.
483
	 *
484
	 * @return string
485
	 *
486
	 * @since 1.21
487
	 */
488
	public function getContentModel() {
489
		if ( $this->exists() ) {
490
			// look at the revision's actual content model
491
			$rev = $this->getRevision();
492
493
			if ( $rev !== null ) {
494
				return $rev->getContentModel();
495
			} else {
496
				$title = $this->mTitle->getPrefixedDBkey();
497
				wfWarn( "Page $title exists but has no (visible) revisions!" );
498
			}
499
		}
500
501
		// use the default model for this page
502
		return $this->mTitle->getContentModel();
503
	}
504
505
	/**
506
	 * Loads page_touched and returns a value indicating if it should be used
507
	 * @return bool True if this page exists and is not a redirect
508
	 */
509
	public function checkTouched() {
510
		if ( !$this->mDataLoaded ) {
511
			$this->loadPageData();
512
		}
513
		return ( $this->mId && !$this->mIsRedirect );
514
	}
515
516
	/**
517
	 * Get the page_touched field
518
	 * @return string Containing GMT timestamp
519
	 */
520
	public function getTouched() {
521
		if ( !$this->mDataLoaded ) {
522
			$this->loadPageData();
523
		}
524
		return $this->mTouched;
525
	}
526
527
	/**
528
	 * Get the page_links_updated field
529
	 * @return string|null Containing GMT timestamp
530
	 */
531
	public function getLinksTimestamp() {
532
		if ( !$this->mDataLoaded ) {
533
			$this->loadPageData();
534
		}
535
		return $this->mLinksUpdated;
536
	}
537
538
	/**
539
	 * Get the page_latest field
540
	 * @return int The rev_id of current revision
541
	 */
542
	public function getLatest() {
543
		if ( !$this->mDataLoaded ) {
544
			$this->loadPageData();
545
		}
546
		return (int)$this->mLatest;
547
	}
548
549
	/**
550
	 * Get the Revision object of the oldest revision
551
	 * @return Revision|null
552
	 */
553
	public function getOldestRevision() {
554
555
		// Try using the slave database first, then try the master
556
		$continue = 2;
557
		$db = wfGetDB( DB_SLAVE );
558
		$revSelectFields = Revision::selectFields();
559
560
		$row = null;
561
		while ( $continue ) {
562
			$row = $db->selectRow(
563
				[ 'page', 'revision' ],
564
				$revSelectFields,
565
				[
566
					'page_namespace' => $this->mTitle->getNamespace(),
567
					'page_title' => $this->mTitle->getDBkey(),
568
					'rev_page = page_id'
569
				],
570
				__METHOD__,
571
				[
572
					'ORDER BY' => 'rev_timestamp ASC'
573
				]
574
			);
575
576
			if ( $row ) {
577
				$continue = 0;
578
			} else {
579
				$db = wfGetDB( DB_MASTER );
580
				$continue--;
581
			}
582
		}
583
584
		return $row ? Revision::newFromRow( $row ) : null;
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type boolean; however, Revision::newFromRow() does only seem to accept object, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
585
	}
586
587
	/**
588
	 * Loads everything except the text
589
	 * This isn't necessary for all uses, so it's only done if needed.
590
	 */
591
	protected function loadLastEdit() {
592
		if ( $this->mLastRevision !== null ) {
593
			return; // already loaded
594
		}
595
596
		$latest = $this->getLatest();
597
		if ( !$latest ) {
598
			return; // page doesn't exist or is missing page_latest info
599
		}
600
601
		if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
602
			// Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
603
			// includes the latest changes committed. This is true even within REPEATABLE-READ
604
			// transactions, where S1 normally only sees changes committed before the first S1
605
			// SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
606
			// may not find it since a page row UPDATE and revision row INSERT by S2 may have
607
			// happened after the first S1 SELECT.
608
			// http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
609
			$flags = Revision::READ_LOCKING;
610
		} elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
611
			// Bug T93976: if page_latest was loaded from the master, fetch the
612
			// revision from there as well, as it may not exist yet on a slave DB.
613
			// Also, this keeps the queries in the same REPEATABLE-READ snapshot.
614
			$flags = Revision::READ_LATEST;
615
		} else {
616
			$flags = 0;
617
		}
618
		$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
619
		if ( $revision ) { // sanity
620
			$this->setLastEdit( $revision );
621
		}
622
	}
623
624
	/**
625
	 * Set the latest revision
626
	 * @param Revision $revision
627
	 */
628
	protected function setLastEdit( Revision $revision ) {
629
		$this->mLastRevision = $revision;
630
		$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...
631
	}
632
633
	/**
634
	 * Get the latest revision
635
	 * @return Revision|null
636
	 */
637
	public function getRevision() {
638
		$this->loadLastEdit();
639
		if ( $this->mLastRevision ) {
640
			return $this->mLastRevision;
641
		}
642
		return null;
643
	}
644
645
	/**
646
	 * Get the content of the current revision. No side-effects...
647
	 *
648
	 * @param int $audience One of:
649
	 *   Revision::FOR_PUBLIC       to be displayed to all users
650
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
651
	 *   Revision::RAW              get the text regardless of permissions
652
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
653
	 *   to the $audience parameter
654
	 * @return Content|null The content of the current revision
655
	 *
656
	 * @since 1.21
657
	 */
658 View Code Duplication
	public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
659
		$this->loadLastEdit();
660
		if ( $this->mLastRevision ) {
661
			return $this->mLastRevision->getContent( $audience, $user );
662
		}
663
		return null;
664
	}
665
666
	/**
667
	 * Get the text of the current revision. No side-effects...
668
	 *
669
	 * @param int $audience One of:
670
	 *   Revision::FOR_PUBLIC       to be displayed to all users
671
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
672
	 *   Revision::RAW              get the text regardless of permissions
673
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
674
	 *   to the $audience parameter
675
	 * @return string|bool The text of the current revision
676
	 * @deprecated since 1.21, getContent() should be used instead.
677
	 */
678
	public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
679
		ContentHandler::deprecated( __METHOD__, '1.21' );
680
681
		$this->loadLastEdit();
682
		if ( $this->mLastRevision ) {
683
			return $this->mLastRevision->getText( $audience, $user );
684
		}
685
		return false;
686
	}
687
688
	/**
689
	 * @return string MW timestamp of last article revision
690
	 */
691
	public function getTimestamp() {
692
		// Check if the field has been filled by WikiPage::setTimestamp()
693
		if ( !$this->mTimestamp ) {
694
			$this->loadLastEdit();
695
		}
696
697
		return wfTimestamp( TS_MW, $this->mTimestamp );
698
	}
699
700
	/**
701
	 * Set the page timestamp (use only to avoid DB queries)
702
	 * @param string $ts MW timestamp of last article revision
703
	 * @return void
704
	 */
705
	public function setTimestamp( $ts ) {
706
		$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...
707
	}
708
709
	/**
710
	 * @param int $audience One of:
711
	 *   Revision::FOR_PUBLIC       to be displayed to all users
712
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
713
	 *   Revision::RAW              get the text regardless of permissions
714
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
715
	 *   to the $audience parameter
716
	 * @return int User ID for the user that made the last article revision
717
	 */
718 View Code Duplication
	public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
719
		$this->loadLastEdit();
720
		if ( $this->mLastRevision ) {
721
			return $this->mLastRevision->getUser( $audience, $user );
722
		} else {
723
			return -1;
724
		}
725
	}
726
727
	/**
728
	 * Get the User object of the user who created the page
729
	 * @param int $audience One of:
730
	 *   Revision::FOR_PUBLIC       to be displayed to all users
731
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
732
	 *   Revision::RAW              get the text regardless of permissions
733
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
734
	 *   to the $audience parameter
735
	 * @return User|null
736
	 */
737
	public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
738
		$revision = $this->getOldestRevision();
739
		if ( $revision ) {
740
			$userName = $revision->getUserText( $audience, $user );
741
			return User::newFromName( $userName, false );
0 ignored issues
show
Bug introduced by
It seems like $userName defined by $revision->getUserText($audience, $user) on line 740 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...
742
		} else {
743
			return null;
744
		}
745
	}
746
747
	/**
748
	 * @param int $audience One of:
749
	 *   Revision::FOR_PUBLIC       to be displayed to all users
750
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
751
	 *   Revision::RAW              get the text regardless of permissions
752
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
753
	 *   to the $audience parameter
754
	 * @return string Username of the user that made the last article revision
755
	 */
756 View Code Duplication
	public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
757
		$this->loadLastEdit();
758
		if ( $this->mLastRevision ) {
759
			return $this->mLastRevision->getUserText( $audience, $user );
760
		} else {
761
			return '';
762
		}
763
	}
764
765
	/**
766
	 * @param int $audience One of:
767
	 *   Revision::FOR_PUBLIC       to be displayed to all users
768
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
769
	 *   Revision::RAW              get the text regardless of permissions
770
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
771
	 *   to the $audience parameter
772
	 * @return string Comment stored for the last article revision
773
	 */
774 View Code Duplication
	public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
775
		$this->loadLastEdit();
776
		if ( $this->mLastRevision ) {
777
			return $this->mLastRevision->getComment( $audience, $user );
778
		} else {
779
			return '';
780
		}
781
	}
782
783
	/**
784
	 * Returns true if last revision was marked as "minor edit"
785
	 *
786
	 * @return bool Minor edit indicator for the last article revision.
787
	 */
788
	public function getMinorEdit() {
789
		$this->loadLastEdit();
790
		if ( $this->mLastRevision ) {
791
			return $this->mLastRevision->isMinor();
792
		} else {
793
			return false;
794
		}
795
	}
796
797
	/**
798
	 * Determine whether a page would be suitable for being counted as an
799
	 * article in the site_stats table based on the title & its content
800
	 *
801
	 * @param object|bool $editInfo (false): object returned by prepareTextForEdit(),
802
	 *   if false, the current database state will be used
803
	 * @return bool
804
	 */
805
	public function isCountable( $editInfo = false ) {
806
		global $wgArticleCountMethod;
807
808
		if ( !$this->mTitle->isContentPage() ) {
809
			return false;
810
		}
811
812
		if ( $editInfo ) {
813
			$content = $editInfo->pstContent;
814
		} else {
815
			$content = $this->getContent();
816
		}
817
818
		if ( !$content || $content->isRedirect() ) {
819
			return false;
820
		}
821
822
		$hasLinks = null;
823
824
		if ( $wgArticleCountMethod === 'link' ) {
825
			// nasty special case to avoid re-parsing to detect links
826
827
			if ( $editInfo ) {
828
				// ParserOutput::getLinks() is a 2D array of page links, so
829
				// to be really correct we would need to recurse in the array
830
				// but the main array should only have items in it if there are
831
				// links.
832
				$hasLinks = (bool)count( $editInfo->output->getLinks() );
833
			} else {
834
				$hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
835
					[ 'pl_from' => $this->getId() ], __METHOD__ );
836
			}
837
		}
838
839
		return $content->isCountable( $hasLinks );
840
	}
841
842
	/**
843
	 * If this page is a redirect, get its target
844
	 *
845
	 * The target will be fetched from the redirect table if possible.
846
	 * If this page doesn't have an entry there, call insertRedirect()
847
	 * @return Title|null Title object, or null if this page is not a redirect
848
	 */
849
	public function getRedirectTarget() {
850
		if ( !$this->mTitle->isRedirect() ) {
851
			return null;
852
		}
853
854
		if ( $this->mRedirectTarget !== null ) {
855
			return $this->mRedirectTarget;
856
		}
857
858
		// Query the redirect table
859
		$dbr = wfGetDB( DB_SLAVE );
860
		$row = $dbr->selectRow( 'redirect',
861
			[ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
862
			[ 'rd_from' => $this->getId() ],
863
			__METHOD__
864
		);
865
866
		// rd_fragment and rd_interwiki were added later, populate them if empty
867
		if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
868
			$this->mRedirectTarget = Title::makeTitle(
869
				$row->rd_namespace, $row->rd_title,
870
				$row->rd_fragment, $row->rd_interwiki
871
			);
872
			return $this->mRedirectTarget;
873
		}
874
875
		// This page doesn't have an entry in the redirect table
876
		$this->mRedirectTarget = $this->insertRedirect();
877
		return $this->mRedirectTarget;
878
	}
879
880
	/**
881
	 * Insert an entry for this page into the redirect table if the content is a redirect
882
	 *
883
	 * The database update will be deferred via DeferredUpdates
884
	 *
885
	 * Don't call this function directly unless you know what you're doing.
886
	 * @return Title|null Title object or null if not a redirect
887
	 */
888
	public function insertRedirect() {
889
		$content = $this->getContent();
890
		$retval = $content ? $content->getUltimateRedirectTarget() : null;
891
		if ( !$retval ) {
892
			return null;
893
		}
894
895
		// Update the DB post-send if the page has not cached since now
896
		$that = $this;
897
		$latest = $this->getLatest();
898
		DeferredUpdates::addCallableUpdate(
899
			function () use ( $that, $retval, $latest ) {
900
				$that->insertRedirectEntry( $retval, $latest );
901
			},
902
			DeferredUpdates::POSTSEND,
903
			wfGetDB( DB_MASTER )
904
		);
905
906
		return $retval;
907
	}
908
909
	/**
910
	 * Insert or update the redirect table entry for this page to indicate it redirects to $rt
911
	 * @param Title $rt Redirect target
912
	 * @param int|null $oldLatest Prior page_latest for check and set
913
	 */
914
	public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
915
		$dbw = wfGetDB( DB_MASTER );
916
		$dbw->startAtomic( __METHOD__ );
917
918
		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...
919
			$dbw->replace( 'redirect',
920
				[ 'rd_from' ],
921
				[
922
					'rd_from' => $this->getId(),
923
					'rd_namespace' => $rt->getNamespace(),
924
					'rd_title' => $rt->getDBkey(),
925
					'rd_fragment' => $rt->getFragment(),
926
					'rd_interwiki' => $rt->getInterwiki(),
927
				],
928
				__METHOD__
929
			);
930
		}
931
932
		$dbw->endAtomic( __METHOD__ );
933
	}
934
935
	/**
936
	 * Get the Title object or URL this page redirects to
937
	 *
938
	 * @return bool|Title|string False, Title of in-wiki target, or string with URL
939
	 */
940
	public function followRedirect() {
941
		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...
942
	}
943
944
	/**
945
	 * Get the Title object or URL to use for a redirect. We use Title
946
	 * objects for same-wiki, non-special redirects and URLs for everything
947
	 * else.
948
	 * @param Title $rt Redirect target
949
	 * @return bool|Title|string False, Title object of local target, or string with URL
950
	 */
951
	public function getRedirectURL( $rt ) {
952
		if ( !$rt ) {
953
			return false;
954
		}
955
956
		if ( $rt->isExternal() ) {
957
			if ( $rt->isLocal() ) {
958
				// Offsite wikis need an HTTP redirect.
959
				// This can be hard to reverse and may produce loops,
960
				// so they may be disabled in the site configuration.
961
				$source = $this->mTitle->getFullURL( 'redirect=no' );
962
				return $rt->getFullURL( [ 'rdfrom' => $source ] );
963
			} else {
964
				// External pages without "local" bit set are not valid
965
				// redirect targets
966
				return false;
967
			}
968
		}
969
970
		if ( $rt->isSpecialPage() ) {
971
			// Gotta handle redirects to special pages differently:
972
			// Fill the HTTP response "Location" header and ignore the rest of the page we're on.
973
			// Some pages are not valid targets.
974
			if ( $rt->isValidRedirectTarget() ) {
975
				return $rt->getFullURL();
976
			} else {
977
				return false;
978
			}
979
		}
980
981
		return $rt;
982
	}
983
984
	/**
985
	 * Get a list of users who have edited this article, not including the user who made
986
	 * the most recent revision, which you can get from $article->getUser() if you want it
987
	 * @return UserArrayFromResult
988
	 */
989
	public function getContributors() {
990
		// @todo FIXME: This is expensive; cache this info somewhere.
991
992
		$dbr = wfGetDB( DB_SLAVE );
993
994
		if ( $dbr->implicitGroupby() ) {
995
			$realNameField = 'user_real_name';
996
		} else {
997
			$realNameField = 'MIN(user_real_name) AS user_real_name';
998
		}
999
1000
		$tables = [ 'revision', 'user' ];
1001
1002
		$fields = [
1003
			'user_id' => 'rev_user',
1004
			'user_name' => 'rev_user_text',
1005
			$realNameField,
1006
			'timestamp' => 'MAX(rev_timestamp)',
1007
		];
1008
1009
		$conds = [ 'rev_page' => $this->getId() ];
1010
1011
		// The user who made the top revision gets credited as "this page was last edited by
1012
		// John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1013
		$user = $this->getUser();
1014
		if ( $user ) {
1015
			$conds[] = "rev_user != $user";
1016
		} else {
1017
			$conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
0 ignored issues
show
Bug introduced by
It seems like $this->getUserText() targeting WikiPage::getUserText() can also be of type boolean; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, 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...
1018
		}
1019
1020
		// Username hidden?
1021
		$conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1022
1023
		$jconds = [
1024
			'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1025
		];
1026
1027
		$options = [
1028
			'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1029
			'ORDER BY' => 'timestamp DESC',
1030
		];
1031
1032
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1033
		return new UserArrayFromResult( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select($tables, $f...D__, $options, $jconds) on line 1032 can also be of type boolean; however, UserArrayFromResult::__construct() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1034
	}
1035
1036
	/**
1037
	 * Should the parser cache be used?
1038
	 *
1039
	 * @param ParserOptions $parserOptions ParserOptions to check
1040
	 * @param int $oldId
1041
	 * @return bool
1042
	 */
1043
	public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1044
		return $parserOptions->getStubThreshold() == 0
1045
			&& $this->exists()
1046
			&& ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1047
			&& $this->getContentHandler()->isParserCacheSupported();
1048
	}
1049
1050
	/**
1051
	 * Get a ParserOutput for the given ParserOptions and revision ID.
1052
	 *
1053
	 * The parser cache will be used if possible. Cache misses that result
1054
	 * in parser runs are debounced with PoolCounter.
1055
	 *
1056
	 * @since 1.19
1057
	 * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
1058
	 * @param null|int      $oldid Revision ID to get the text from, passing null or 0 will
1059
	 *                             get the current revision (default value)
1060
	 * @param bool          $forceParse Force reindexing, regardless of cache settings
1061
	 * @return bool|ParserOutput ParserOutput or false if the revision was not found
1062
	 */
1063
	public function getParserOutput(
1064
		ParserOptions $parserOptions, $oldid = null, $forceParse = false
1065
	) {
1066
		$useParserCache =
1067
			( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
1068
		wfDebug( __METHOD__ .
1069
			': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1070
		if ( $parserOptions->getStubThreshold() ) {
1071
			wfIncrStats( 'pcache.miss.stub' );
1072
		}
1073
1074
		if ( $useParserCache ) {
1075
			$parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1076
			if ( $parserOutput !== false ) {
1077
				return $parserOutput;
1078
			}
1079
		}
1080
1081
		if ( $oldid === null || $oldid === 0 ) {
1082
			$oldid = $this->getLatest();
1083
		}
1084
1085
		$pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1086
		$pool->execute();
1087
1088
		return $pool->getParserOutput();
1089
	}
1090
1091
	/**
1092
	 * Do standard deferred updates after page view (existing or missing page)
1093
	 * @param User $user The relevant user
1094
	 * @param int $oldid Revision id being viewed; if not given or 0, latest revision is assumed
1095
	 */
1096
	public function doViewUpdates( User $user, $oldid = 0 ) {
1097
		if ( wfReadOnly() ) {
1098
			return;
1099
		}
1100
1101
		Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1102
		// Update newtalk / watchlist notification status
1103
		try {
1104
			$user->clearNotification( $this->mTitle, $oldid );
1105
		} catch ( DBError $e ) {
1106
			// Avoid outage if the master is not reachable
1107
			MWExceptionHandler::logException( $e );
1108
		}
1109
	}
1110
1111
	/**
1112
	 * Perform the actions of a page purging
1113
	 * @return bool
1114
	 */
1115
	public function doPurge() {
1116
		if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
1117
			return false;
1118
		}
1119
1120
		$this->mTitle->invalidateCache();
1121
		// Send purge after above page_touched update was committed
1122
		DeferredUpdates::addUpdate(
1123
			new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
1124
			DeferredUpdates::PRESEND
1125
		);
1126
1127
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1128
			// @todo move this logic to MessageCache
1129
			if ( $this->exists() ) {
1130
				// NOTE: use transclusion text for messages.
1131
				//       This is consistent with  MessageCache::getMsgFromNamespace()
1132
1133
				$content = $this->getContent();
1134
				$text = $content === null ? null : $content->getWikitextForTransclusion();
1135
1136
				if ( $text === null ) {
1137
					$text = false;
1138
				}
1139
			} else {
1140
				$text = false;
1141
			}
1142
1143
			MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1144
		}
1145
1146
		return true;
1147
	}
1148
1149
	/**
1150
	 * Insert a new empty page record for this article.
1151
	 * This *must* be followed up by creating a revision
1152
	 * and running $this->updateRevisionOn( ... );
1153
	 * or else the record will be left in a funky state.
1154
	 * Best if all done inside a transaction.
1155
	 *
1156
	 * @param IDatabase $dbw
1157
	 * @param int|null $pageId Custom page ID that will be used for the insert statement
1158
	 *
1159
	 * @return bool|int The newly created page_id key; false if the row was not
1160
	 *   inserted, e.g. because the title already existed or because the specified
1161
	 *   page ID is already in use.
1162
	 */
1163
	public function insertOn( $dbw, $pageId = null ) {
1164
		$pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
1165
		$dbw->insert(
1166
			'page',
1167
			[
1168
				'page_id'           => $pageIdForInsert,
1169
				'page_namespace'    => $this->mTitle->getNamespace(),
1170
				'page_title'        => $this->mTitle->getDBkey(),
1171
				'page_restrictions' => '',
1172
				'page_is_redirect'  => 0, // Will set this shortly...
1173
				'page_is_new'       => 1,
1174
				'page_random'       => wfRandom(),
1175
				'page_touched'      => $dbw->timestamp(),
1176
				'page_latest'       => 0, // Fill this in shortly...
1177
				'page_len'          => 0, // Fill this in shortly...
1178
			],
1179
			__METHOD__,
1180
			'IGNORE'
1181
		);
1182
1183
		if ( $dbw->affectedRows() > 0 ) {
1184
			$newid = $pageId ?: $dbw->insertId();
1185
			$this->mId = $newid;
1186
			$this->mTitle->resetArticleID( $newid );
1187
1188
			return $newid;
1189
		} else {
1190
			return false; // nothing changed
1191
		}
1192
	}
1193
1194
	/**
1195
	 * Update the page record to point to a newly saved revision.
1196
	 *
1197
	 * @param IDatabase $dbw
1198
	 * @param Revision $revision For ID number, and text used to set
1199
	 *   length and redirect status fields
1200
	 * @param int $lastRevision If given, will not overwrite the page field
1201
	 *   when different from the currently set value.
1202
	 *   Giving 0 indicates the new page flag should be set on.
1203
	 * @param bool $lastRevIsRedirect If given, will optimize adding and
1204
	 *   removing rows in redirect table.
1205
	 * @return bool Success; false if the page row was missing or page_latest changed
1206
	 */
1207
	public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1208
		$lastRevIsRedirect = null
1209
	) {
1210
		global $wgContentHandlerUseDB;
1211
1212
		// Assertion to try to catch T92046
1213
		if ( (int)$revision->getId() === 0 ) {
1214
			throw new InvalidArgumentException(
1215
				__METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1216
			);
1217
		}
1218
1219
		$content = $revision->getContent();
1220
		$len = $content ? $content->getSize() : 0;
1221
		$rt = $content ? $content->getUltimateRedirectTarget() : null;
1222
1223
		$conditions = [ 'page_id' => $this->getId() ];
1224
1225
		if ( !is_null( $lastRevision ) ) {
1226
			// An extra check against threads stepping on each other
1227
			$conditions['page_latest'] = $lastRevision;
1228
		}
1229
1230
		$row = [ /* SET */
1231
			'page_latest'      => $revision->getId(),
1232
			'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
1233
			'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
1234
			'page_is_redirect' => $rt !== null ? 1 : 0,
1235
			'page_len'         => $len,
1236
		];
1237
1238
		if ( $wgContentHandlerUseDB ) {
1239
			$row['page_content_model'] = $revision->getContentModel();
1240
		}
1241
1242
		$dbw->update( 'page',
1243
			$row,
1244
			$conditions,
1245
			__METHOD__ );
1246
1247
		$result = $dbw->affectedRows() > 0;
1248
		if ( $result ) {
1249
			$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 1221 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...
1250
			$this->setLastEdit( $revision );
1251
			$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...
1252
			$this->mIsRedirect = (bool)$rt;
1253
			// Update the LinkCache.
1254
			LinkCache::singleton()->addGoodLinkObj(
1255
				$this->getId(),
1256
				$this->mTitle,
1257
				$len,
1258
				$this->mIsRedirect,
1259
				$this->mLatest,
1260
				$revision->getContentModel()
1261
			);
1262
		}
1263
1264
		return $result;
1265
	}
1266
1267
	/**
1268
	 * Add row to the redirect table if this is a redirect, remove otherwise.
1269
	 *
1270
	 * @param IDatabase $dbw
1271
	 * @param Title $redirectTitle Title object pointing to the redirect target,
1272
	 *   or NULL if this is not a redirect
1273
	 * @param null|bool $lastRevIsRedirect If given, will optimize adding and
1274
	 *   removing rows in redirect table.
1275
	 * @return bool True on success, false on failure
1276
	 * @private
1277
	 */
1278
	public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1279
		// Always update redirects (target link might have changed)
1280
		// Update/Insert if we don't know if the last revision was a redirect or not
1281
		// Delete if changing from redirect to non-redirect
1282
		$isRedirect = !is_null( $redirectTitle );
1283
1284
		if ( !$isRedirect && $lastRevIsRedirect === false ) {
1285
			return true;
1286
		}
1287
1288
		if ( $isRedirect ) {
1289
			$this->insertRedirectEntry( $redirectTitle );
1290
		} else {
1291
			// This is not a redirect, remove row from redirect table
1292
			$where = [ 'rd_from' => $this->getId() ];
1293
			$dbw->delete( 'redirect', $where, __METHOD__ );
1294
		}
1295
1296
		if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1297
			RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1298
		}
1299
1300
		return ( $dbw->affectedRows() != 0 );
1301
	}
1302
1303
	/**
1304
	 * If the given revision is newer than the currently set page_latest,
1305
	 * update the page record. Otherwise, do nothing.
1306
	 *
1307
	 * @deprecated since 1.24, use updateRevisionOn instead
1308
	 *
1309
	 * @param IDatabase $dbw
1310
	 * @param Revision $revision
1311
	 * @return bool
1312
	 */
1313
	public function updateIfNewerOn( $dbw, $revision ) {
1314
1315
		$row = $dbw->selectRow(
1316
			[ 'revision', 'page' ],
1317
			[ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1318
			[
1319
				'page_id' => $this->getId(),
1320
				'page_latest=rev_id' ],
1321
			__METHOD__ );
1322
1323
		if ( $row ) {
1324
			if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1325
				return false;
1326
			}
1327
			$prev = $row->rev_id;
1328
			$lastRevIsRedirect = (bool)$row->page_is_redirect;
1329
		} else {
1330
			// No or missing previous revision; mark the page as new
1331
			$prev = 0;
1332
			$lastRevIsRedirect = null;
1333
		}
1334
1335
		$ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1336
1337
		return $ret;
1338
	}
1339
1340
	/**
1341
	 * Get the content that needs to be saved in order to undo all revisions
1342
	 * between $undo and $undoafter. Revisions must belong to the same page,
1343
	 * must exist and must not be deleted
1344
	 * @param Revision $undo
1345
	 * @param Revision $undoafter Must be an earlier revision than $undo
1346
	 * @return Content|bool Content on success, false on failure
1347
	 * @since 1.21
1348
	 * Before we had the Content object, this was done in getUndoText
1349
	 */
1350
	public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1351
		$handler = $undo->getContentHandler();
1352
		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 1350 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...
1353
	}
1354
1355
	/**
1356
	 * Returns true if this page's content model supports sections.
1357
	 *
1358
	 * @return bool
1359
	 *
1360
	 * @todo The skin should check this and not offer section functionality if
1361
	 *   sections are not supported.
1362
	 * @todo The EditPage should check this and not offer section functionality
1363
	 *   if sections are not supported.
1364
	 */
1365
	public function supportsSections() {
1366
		return $this->getContentHandler()->supportsSections();
1367
	}
1368
1369
	/**
1370
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1371
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1372
	 * or 'new' for a new section.
1373
	 * @param Content $sectionContent New content of the section.
1374
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1375
	 * @param string $edittime Revision timestamp or null to use the current revision.
1376
	 *
1377
	 * @throws MWException
1378
	 * @return Content|null New complete article content, or null if error.
1379
	 *
1380
	 * @since 1.21
1381
	 * @deprecated since 1.24, use replaceSectionAtRev instead
1382
	 */
1383
	public function replaceSectionContent(
1384
		$sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1385
	) {
1386
1387
		$baseRevId = null;
1388
		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...
1389
			$dbr = wfGetDB( DB_SLAVE );
1390
			$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1391
			// Try the master if this thread may have just added it.
1392
			// This could be abstracted into a Revision method, but we don't want
1393
			// to encourage loading of revisions by timestamp.
1394
			if ( !$rev
1395
				&& 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...
1396
				&& 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...
1397
			) {
1398
				$dbw = wfGetDB( DB_MASTER );
1399
				$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1400
			}
1401
			if ( $rev ) {
1402
				$baseRevId = $rev->getId();
1403
			}
1404
		}
1405
1406
		return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1407
	}
1408
1409
	/**
1410
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1411
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1412
	 * or 'new' for a new section.
1413
	 * @param Content $sectionContent New content of the section.
1414
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1415
	 * @param int|null $baseRevId
1416
	 *
1417
	 * @throws MWException
1418
	 * @return Content|null New complete article content, or null if error.
1419
	 *
1420
	 * @since 1.24
1421
	 */
1422
	public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1423
		$sectionTitle = '', $baseRevId = null
1424
	) {
1425
1426
		if ( strval( $sectionId ) === '' ) {
1427
			// Whole-page edit; let the whole text through
1428
			$newContent = $sectionContent;
1429
		} else {
1430
			if ( !$this->supportsSections() ) {
1431
				throw new MWException( "sections not supported for content model " .
1432
					$this->getContentHandler()->getModelID() );
1433
			}
1434
1435
			// Bug 30711: always use current version when adding a new section
1436
			if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1437
				$oldContent = $this->getContent();
1438
			} else {
1439
				$rev = Revision::newFromId( $baseRevId );
1440
				if ( !$rev ) {
1441
					wfDebug( __METHOD__ . " asked for bogus section (page: " .
1442
						$this->getId() . "; section: $sectionId)\n" );
1443
					return null;
1444
				}
1445
1446
				$oldContent = $rev->getContent();
1447
			}
1448
1449
			if ( !$oldContent ) {
1450
				wfDebug( __METHOD__ . ": no page text\n" );
1451
				return null;
1452
			}
1453
1454
			$newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1455
		}
1456
1457
		return $newContent;
1458
	}
1459
1460
	/**
1461
	 * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
1462
	 * @param int $flags
1463
	 * @return int Updated $flags
1464
	 */
1465
	public function checkFlags( $flags ) {
1466
		if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1467
			if ( $this->exists() ) {
1468
				$flags |= EDIT_UPDATE;
1469
			} else {
1470
				$flags |= EDIT_NEW;
1471
			}
1472
		}
1473
1474
		return $flags;
1475
	}
1476
1477
	/**
1478
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1479
	 * optionally via the deferred update array.
1480
	 *
1481
	 * @param string $text New text
1482
	 * @param string $summary Edit summary
1483
	 * @param int $flags Bitfield:
1484
	 *      EDIT_NEW
1485
	 *          Article is known or assumed to be non-existent, create a new one
1486
	 *      EDIT_UPDATE
1487
	 *          Article is known or assumed to be pre-existing, update it
1488
	 *      EDIT_MINOR
1489
	 *          Mark this edit minor, if the user is allowed to do so
1490
	 *      EDIT_SUPPRESS_RC
1491
	 *          Do not log the change in recentchanges
1492
	 *      EDIT_FORCE_BOT
1493
	 *          Mark the edit a "bot" edit regardless of user rights
1494
	 *      EDIT_AUTOSUMMARY
1495
	 *          Fill in blank summaries with generated text where possible
1496
	 *      EDIT_INTERNAL
1497
	 *          Signal that the page retrieve/save cycle happened entirely in this request.
1498
	 *
1499
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1500
	 * article will be detected. If EDIT_UPDATE is specified and the article
1501
	 * doesn't exist, the function will return an edit-gone-missing error. If
1502
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1503
	 * error will be returned. These two conditions are also possible with
1504
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1505
	 *
1506
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1507
	 *   This is not the parent revision ID, rather the revision ID for older
1508
	 *   content used as the source for a rollback, for example.
1509
	 * @param User $user The user doing the edit
1510
	 *
1511
	 * @throws MWException
1512
	 * @return Status Possible errors:
1513
	 *   edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1514
	 *     set the fatal flag of $status
1515
	 *   edit-gone-missing: In update mode, but the article didn't exist.
1516
	 *   edit-conflict: In update mode, the article changed unexpectedly.
1517
	 *   edit-no-change: Warning that the text was the same as before.
1518
	 *   edit-already-exists: In creation mode, but the article already exists.
1519
	 *
1520
	 * Extensions may define additional errors.
1521
	 *
1522
	 * $return->value will contain an associative array with members as follows:
1523
	 *     new: Boolean indicating if the function attempted to create a new article.
1524
	 *     revision: The revision object for the inserted revision, or null.
1525
	 *
1526
	 * Compatibility note: this function previously returned a boolean value
1527
	 * indicating success/failure
1528
	 *
1529
	 * @deprecated since 1.21: use doEditContent() instead.
1530
	 */
1531
	public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1532
		ContentHandler::deprecated( __METHOD__, '1.21' );
1533
1534
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
1535
1536
		return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1537
	}
1538
1539
	/**
1540
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1541
	 * optionally via the deferred update array.
1542
	 *
1543
	 * @param Content $content New content
1544
	 * @param string $summary Edit summary
1545
	 * @param int $flags Bitfield:
1546
	 *      EDIT_NEW
1547
	 *          Article is known or assumed to be non-existent, create a new one
1548
	 *      EDIT_UPDATE
1549
	 *          Article is known or assumed to be pre-existing, update it
1550
	 *      EDIT_MINOR
1551
	 *          Mark this edit minor, if the user is allowed to do so
1552
	 *      EDIT_SUPPRESS_RC
1553
	 *          Do not log the change in recentchanges
1554
	 *      EDIT_FORCE_BOT
1555
	 *          Mark the edit a "bot" edit regardless of user rights
1556
	 *      EDIT_AUTOSUMMARY
1557
	 *          Fill in blank summaries with generated text where possible
1558
	 *      EDIT_INTERNAL
1559
	 *          Signal that the page retrieve/save cycle happened entirely in this request.
1560
	 *
1561
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1562
	 * article will be detected. If EDIT_UPDATE is specified and the article
1563
	 * doesn't exist, the function will return an edit-gone-missing error. If
1564
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1565
	 * error will be returned. These two conditions are also possible with
1566
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1567
	 *
1568
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1569
	 *   This is not the parent revision ID, rather the revision ID for older
1570
	 *   content used as the source for a rollback, for example.
1571
	 * @param User $user The user doing the edit
1572
	 * @param string $serialFormat Format for storing the content in the
1573
	 *   database.
1574
	 * @param array|null $tags Change tags to apply to this edit
1575
	 * Callers are responsible for permission checks
1576
	 * (with ChangeTags::canAddTagsAccompanyingChange)
1577
	 *
1578
	 * @throws MWException
1579
	 * @return Status Possible errors:
1580
	 *     edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1581
	 *       set the fatal flag of $status.
1582
	 *     edit-gone-missing: In update mode, but the article didn't exist.
1583
	 *     edit-conflict: In update mode, the article changed unexpectedly.
1584
	 *     edit-no-change: Warning that the text was the same as before.
1585
	 *     edit-already-exists: In creation mode, but the article already exists.
1586
	 *
1587
	 *  Extensions may define additional errors.
1588
	 *
1589
	 *  $return->value will contain an associative array with members as follows:
1590
	 *     new: Boolean indicating if the function attempted to create a new article.
1591
	 *     revision: The revision object for the inserted revision, or null.
1592
	 *
1593
	 * @since 1.21
1594
	 * @throws MWException
1595
	 */
1596
	public function doEditContent(
1597
		Content $content, $summary, $flags = 0, $baseRevId = false,
1598
		User $user = null, $serialFormat = null, $tags = null
1599
	) {
1600
		global $wgUser, $wgUseAutomaticEditSummaries;
1601
1602
		// Low-level sanity check
1603
		if ( $this->mTitle->getText() === '' ) {
1604
			throw new MWException( 'Something is trying to edit an article with an empty title' );
1605
		}
1606
		// Make sure the given content type is allowed for this page
1607
		if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1608
			return Status::newFatal( 'content-not-allowed-here',
1609
				ContentHandler::getLocalizedName( $content->getModel() ),
1610
				$this->mTitle->getPrefixedText()
1611
			);
1612
		}
1613
1614
		// Load the data from the master database if needed.
1615
		// The caller may already loaded it from the master or even loaded it using
1616
		// SELECT FOR UPDATE, so do not override that using clear().
1617
		$this->loadPageData( 'fromdbmaster' );
1618
1619
		$user = $user ?: $wgUser;
1620
		$flags = $this->checkFlags( $flags );
1621
1622
		// Trigger pre-save hook (using provided edit summary)
1623
		$hookStatus = Status::newGood( [] );
1624
		$hook_args = [ &$this, &$user, &$content, &$summary,
1625
							$flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1626
		// Check if the hook rejected the attempted save
1627
		if ( !Hooks::run( 'PageContentSave', $hook_args )
1628
			|| !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
1629
		) {
1630
			if ( $hookStatus->isOK() ) {
1631
				// Hook returned false but didn't call fatal(); use generic message
1632
				$hookStatus->fatal( 'edit-hook-aborted' );
1633
			}
1634
1635
			return $hookStatus;
1636
		}
1637
1638
		$old_revision = $this->getRevision(); // current revision
1639
		$old_content = $this->getContent( Revision::RAW ); // current revision's content
1640
1641
		// Provide autosummaries if one is not provided and autosummaries are enabled
1642
		if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1643
			$handler = $content->getContentHandler();
1644
			$summary = $handler->getAutosummary( $old_content, $content, $flags );
1645
		}
1646
1647
		// Avoid statsd noise and wasted cycles check the edit stash (T136678)
1648
		if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
1649
			$useCache = false;
1650
		} else {
1651
			$useCache = true;
1652
		}
1653
1654
		// Get the pre-save transform content and final parser output
1655
		$editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
1656
		$pstContent = $editInfo->pstContent; // Content object
1657
		$meta = [
1658
			'bot' => ( $flags & EDIT_FORCE_BOT ),
1659
			'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1660
			'serialized' => $editInfo->pst,
1661
			'serialFormat' => $serialFormat,
1662
			'baseRevId' => $baseRevId,
1663
			'oldRevision' => $old_revision,
1664
			'oldContent' => $old_content,
1665
			'oldId' => $this->getLatest(),
1666
			'oldIsRedirect' => $this->isRedirect(),
1667
			'oldCountable' => $this->isCountable(),
1668
			'tags' => ( $tags !== null ) ? (array)$tags : []
1669
		];
1670
1671
		// Actually create the revision and create/update the page
1672
		if ( $flags & EDIT_UPDATE ) {
1673
			$status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1674
		} else {
1675
			$status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1676
		}
1677
1678
		// Promote user to any groups they meet the criteria for
1679
		DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1680
			$user->addAutopromoteOnceGroups( 'onEdit' );
1681
			$user->addAutopromoteOnceGroups( 'onView' ); // b/c
1682
		} );
1683
1684
		return $status;
1685
	}
1686
1687
	/**
1688
	 * @param Content $content Pre-save transform content
1689
	 * @param integer $flags
1690
	 * @param User $user
1691
	 * @param string $summary
1692
	 * @param array $meta
1693
	 * @return Status
1694
	 * @throws DBUnexpectedError
1695
	 * @throws Exception
1696
	 * @throws FatalError
1697
	 * @throws MWException
1698
	 */
1699
	private function doModify(
1700
		Content $content, $flags, User $user, $summary, array $meta
1701
	) {
1702
		global $wgUseRCPatrol;
1703
1704
		// Update article, but only if changed.
1705
		$status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1706
1707
		// Convenience variables
1708
		$now = wfTimestampNow();
1709
		$oldid = $meta['oldId'];
1710
		/** @var $oldContent Content|null */
1711
		$oldContent = $meta['oldContent'];
1712
		$newsize = $content->getSize();
1713
1714
		if ( !$oldid ) {
1715
			// Article gone missing
1716
			$status->fatal( 'edit-gone-missing' );
1717
1718
			return $status;
1719
		} elseif ( !$oldContent ) {
1720
			// Sanity check for bug 37225
1721
			throw new MWException( "Could not find text for current revision {$oldid}." );
1722
		}
1723
1724
		// @TODO: pass content object?!
1725
		$revision = new Revision( [
1726
			'page'       => $this->getId(),
1727
			'title'      => $this->mTitle, // for determining the default content model
1728
			'comment'    => $summary,
1729
			'minor_edit' => $meta['minor'],
1730
			'text'       => $meta['serialized'],
1731
			'len'        => $newsize,
1732
			'parent_id'  => $oldid,
1733
			'user'       => $user->getId(),
1734
			'user_text'  => $user->getName(),
1735
			'timestamp'  => $now,
1736
			'content_model' => $content->getModel(),
1737
			'content_format' => $meta['serialFormat'],
1738
		] );
1739
1740
		$changed = !$content->equals( $oldContent );
1741
1742
		$dbw = wfGetDB( DB_MASTER );
1743
1744
		if ( $changed ) {
1745
			$prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1746
			$status->merge( $prepStatus );
1747
			if ( !$status->isOK() ) {
1748
				return $status;
1749
			}
1750
1751
			$dbw->startAtomic( __METHOD__ );
1752
			// Get the latest page_latest value while locking it.
1753
			// Do a CAS style check to see if it's the same as when this method
1754
			// started. If it changed then bail out before touching the DB.
1755
			$latestNow = $this->lockAndGetLatest();
1756
			if ( $latestNow != $oldid ) {
1757
				$dbw->endAtomic( __METHOD__ );
1758
				// Page updated or deleted in the mean time
1759
				$status->fatal( 'edit-conflict' );
1760
1761
				return $status;
1762
			}
1763
1764
			// At this point we are now comitted to returning an OK
1765
			// status unless some DB query error or other exception comes up.
1766
			// This way callers don't have to call rollback() if $status is bad
1767
			// unless they actually try to catch exceptions (which is rare).
1768
1769
			// Save the revision text
1770
			$revisionId = $revision->insertOn( $dbw );
1771
			// Update page_latest and friends to reflect the new revision
1772
			if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1773
				throw new MWException( "Failed to update page row to use new revision." );
1774
			}
1775
1776
			Hooks::run( 'NewRevisionFromEditComplete',
1777
				[ $this, $revision, $meta['baseRevId'], $user ] );
1778
1779
			// Update recentchanges
1780
			if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1781
				// Mark as patrolled if the user can do so
1782
				$patrolled = $wgUseRCPatrol && !count(
1783
						$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1784
				// Add RC row to the DB
1785
				RecentChange::notifyEdit(
1786
					$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1708 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...
1787
					$this->mTitle,
1788
					$revision->isMinor(),
1789
					$user,
1790
					$summary,
1791
					$oldid,
1792
					$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...
1793
					$meta['bot'],
1794
					'',
1795
					$oldContent ? $oldContent->getSize() : 0,
1796
					$newsize,
1797
					$revisionId,
1798
					$patrolled,
1799
					$meta['tags']
1800
				);
1801
			}
1802
1803
			$user->incEditCount();
1804
1805
			$dbw->endAtomic( __METHOD__ );
1806
			$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...
1807
		} else {
1808
			// Bug 32948: revision ID must be set to page {{REVISIONID}} and
1809
			// related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
1810
			$revision->setId( $this->getLatest() );
1811
			$revision->setUserIdAndName(
1812
				$this->getUser( Revision::RAW ),
1813
				$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...
1814
			);
1815
		}
1816
1817
		if ( $changed ) {
1818
			// Return the new revision to the caller
1819
			$status->value['revision'] = $revision;
1820
		} else {
1821
			$status->warning( 'edit-no-change' );
1822
			// Update page_touched as updateRevisionOn() was not called.
1823
			// Other cache updates are managed in onArticleEdit() via doEditUpdates().
1824
			$this->mTitle->invalidateCache( $now );
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1708 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...
1825
		}
1826
1827
		// Do secondary updates once the main changes have been committed...
1828
		DeferredUpdates::addUpdate(
1829
			new AtomicSectionUpdate(
1830
				$dbw,
1831
				__METHOD__,
1832
				function () use (
1833
					$revision, &$user, $content, $summary, &$flags,
1834
					$changed, $meta, &$status
1835
				) {
1836
					// Update links tables, site stats, etc.
1837
					$this->doEditUpdates(
1838
						$revision,
1839
						$user,
1840
						[
1841
							'changed' => $changed,
1842
							'oldcountable' => $meta['oldCountable'],
1843
							'oldrevision' => $meta['oldRevision']
1844
						]
1845
					);
1846
					// Trigger post-save hook
1847
					$params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
1848
						null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
1849
					ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1850
					Hooks::run( 'PageContentSaveComplete', $params );
1851
				}
1852
			),
1853
			DeferredUpdates::PRESEND
1854
		);
1855
1856
		return $status;
1857
	}
1858
1859
	/**
1860
	 * @param Content $content Pre-save transform content
1861
	 * @param integer $flags
1862
	 * @param User $user
1863
	 * @param string $summary
1864
	 * @param array $meta
1865
	 * @return Status
1866
	 * @throws DBUnexpectedError
1867
	 * @throws Exception
1868
	 * @throws FatalError
1869
	 * @throws MWException
1870
	 */
1871
	private function doCreate(
1872
		Content $content, $flags, User $user, $summary, array $meta
1873
	) {
1874
		global $wgUseRCPatrol, $wgUseNPPatrol;
1875
1876
		$status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1877
1878
		$now = wfTimestampNow();
1879
		$newsize = $content->getSize();
1880
		$prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1881
		$status->merge( $prepStatus );
1882
		if ( !$status->isOK() ) {
1883
			return $status;
1884
		}
1885
1886
		$dbw = wfGetDB( DB_MASTER );
1887
		$dbw->startAtomic( __METHOD__ );
1888
1889
		// Add the page record unless one already exists for the title
1890
		$newid = $this->insertOn( $dbw );
1891
		if ( $newid === false ) {
1892
			$dbw->endAtomic( __METHOD__ ); // nothing inserted
1893
			$status->fatal( 'edit-already-exists' );
1894
1895
			return $status; // nothing done
1896
		}
1897
1898
		// At this point we are now comitted to returning an OK
1899
		// status unless some DB query error or other exception comes up.
1900
		// This way callers don't have to call rollback() if $status is bad
1901
		// unless they actually try to catch exceptions (which is rare).
1902
1903
		// @TODO: pass content object?!
1904
		$revision = new Revision( [
1905
			'page'       => $newid,
1906
			'title'      => $this->mTitle, // for determining the default content model
1907
			'comment'    => $summary,
1908
			'minor_edit' => $meta['minor'],
1909
			'text'       => $meta['serialized'],
1910
			'len'        => $newsize,
1911
			'user'       => $user->getId(),
1912
			'user_text'  => $user->getName(),
1913
			'timestamp'  => $now,
1914
			'content_model' => $content->getModel(),
1915
			'content_format' => $meta['serialFormat'],
1916
		] );
1917
1918
		// Save the revision text...
1919
		$revisionId = $revision->insertOn( $dbw );
1920
		// Update the page record with revision data
1921
		if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1922
			throw new MWException( "Failed to update page row to use new revision." );
1923
		}
1924
1925
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1926
1927
		// Update recentchanges
1928
		if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1929
			// Mark as patrolled if the user can do so
1930
			$patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1931
				!count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1932
			// Add RC row to the DB
1933
			RecentChange::notifyNew(
1934
				$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1878 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...
1935
				$this->mTitle,
1936
				$revision->isMinor(),
1937
				$user,
1938
				$summary,
1939
				$meta['bot'],
1940
				'',
1941
				$newsize,
1942
				$revisionId,
1943
				$patrolled,
1944
				$meta['tags']
1945
			);
1946
		}
1947
1948
		$user->incEditCount();
1949
1950
		$dbw->endAtomic( __METHOD__ );
1951
		$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...
1952
1953
		// Return the new revision to the caller
1954
		$status->value['revision'] = $revision;
1955
1956
		// Do secondary updates once the main changes have been committed...
1957
		DeferredUpdates::addUpdate(
1958
			new AtomicSectionUpdate(
1959
				$dbw,
1960
				__METHOD__,
1961
				function () use (
1962
					$revision, &$user, $content, $summary, &$flags, $meta, &$status
1963
				) {
1964
					// Update links, etc.
1965
					$this->doEditUpdates( $revision, $user, [ 'created' => true ] );
1966
					// Trigger post-create hook
1967
					$params = [ &$this, &$user, $content, $summary,
1968
						$flags & EDIT_MINOR, null, null, &$flags, $revision ];
1969
					ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
1970
					Hooks::run( 'PageContentInsertComplete', $params );
1971
					// Trigger post-save hook
1972
					$params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
1973
					ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1974
					Hooks::run( 'PageContentSaveComplete', $params );
1975
1976
				}
1977
			),
1978
			DeferredUpdates::PRESEND
1979
		);
1980
1981
		return $status;
1982
	}
1983
1984
	/**
1985
	 * Get parser options suitable for rendering the primary article wikitext
1986
	 *
1987
	 * @see ContentHandler::makeParserOptions
1988
	 *
1989
	 * @param IContextSource|User|string $context One of the following:
1990
	 *        - IContextSource: Use the User and the Language of the provided
1991
	 *          context
1992
	 *        - User: Use the provided User object and $wgLang for the language,
1993
	 *          so use an IContextSource object if possible.
1994
	 *        - 'canonical': Canonical options (anonymous user with default
1995
	 *          preferences and content language).
1996
	 * @return ParserOptions
1997
	 */
1998
	public function makeParserOptions( $context ) {
1999
		$options = $this->getContentHandler()->makeParserOptions( $context );
2000
2001
		if ( $this->getTitle()->isConversionTable() ) {
2002
			// @todo ConversionTable should become a separate content model, so
2003
			// we don't need special cases like this one.
2004
			$options->disableContentConversion();
2005
		}
2006
2007
		return $options;
2008
	}
2009
2010
	/**
2011
	 * Prepare text which is about to be saved.
2012
	 * Returns a stdClass with source, pst and output members
2013
	 *
2014
	 * @param string $text
2015
	 * @param int|null $revid
2016
	 * @param User|null $user
2017
	 * @deprecated since 1.21: use prepareContentForEdit instead.
2018
	 * @return object
2019
	 */
2020
	public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
2021
		ContentHandler::deprecated( __METHOD__, '1.21' );
2022
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
2023
		return $this->prepareContentForEdit( $content, $revid, $user );
2024
	}
2025
2026
	/**
2027
	 * Prepare content which is about to be saved.
2028
	 * Returns a stdClass with source, pst and output members
2029
	 *
2030
	 * @param Content $content
2031
	 * @param Revision|int|null $revision Revision object. For backwards compatibility, a
2032
	 *        revision ID is also accepted, but this is deprecated.
2033
	 * @param User|null $user
2034
	 * @param string|null $serialFormat
2035
	 * @param bool $useCache Check shared prepared edit cache
2036
	 *
2037
	 * @return object
2038
	 *
2039
	 * @since 1.21
2040
	 */
2041
	public function prepareContentForEdit(
2042
		Content $content, $revision = null, User $user = null,
2043
		$serialFormat = null, $useCache = true
2044
	) {
2045
		global $wgContLang, $wgUser, $wgAjaxEditStash;
2046
2047
		if ( is_object( $revision ) ) {
2048
			$revid = $revision->getId();
2049
		} else {
2050
			$revid = $revision;
2051
			// This code path is deprecated, and nothing is known to
2052
			// use it, so performance here shouldn't be a worry.
2053
			if ( $revid !== null ) {
2054
				$revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2055
			} else {
2056
				$revision = null;
2057
			}
2058
		}
2059
2060
		$user = is_null( $user ) ? $wgUser : $user;
2061
		// XXX: check $user->getId() here???
2062
2063
		// Use a sane default for $serialFormat, see bug 57026
2064
		if ( $serialFormat === null ) {
2065
			$serialFormat = $content->getContentHandler()->getDefaultFormat();
2066
		}
2067
2068
		if ( $this->mPreparedEdit
2069
			&& isset( $this->mPreparedEdit->newContent )
2070
			&& $this->mPreparedEdit->newContent->equals( $content )
2071
			&& $this->mPreparedEdit->revid == $revid
2072
			&& $this->mPreparedEdit->format == $serialFormat
2073
			// XXX: also check $user here?
2074
		) {
2075
			// Already prepared
2076
			return $this->mPreparedEdit;
2077
		}
2078
2079
		// The edit may have already been prepared via api.php?action=stashedit
2080
		$cachedEdit = $useCache && $wgAjaxEditStash
2081
			? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2082
			: false;
2083
2084
		$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2085
		Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2086
2087
		$edit = (object)[];
2088
		if ( $cachedEdit ) {
2089
			$edit->timestamp = $cachedEdit->timestamp;
2090
		} else {
2091
			$edit->timestamp = wfTimestampNow();
2092
		}
2093
		// @note: $cachedEdit is safely not used if the rev ID was referenced in the text
2094
		$edit->revid = $revid;
2095
2096
		if ( $cachedEdit ) {
2097
			$edit->pstContent = $cachedEdit->pstContent;
2098
		} else {
2099
			$edit->pstContent = $content
2100
				? $content->preSaveTransform( $this->mTitle, $user, $popts )
2101
				: null;
2102
		}
2103
2104
		$edit->format = $serialFormat;
2105
		$edit->popts = $this->makeParserOptions( 'canonical' );
2106
		if ( $cachedEdit ) {
2107
			$edit->output = $cachedEdit->output;
2108
		} else {
2109
			if ( $revision ) {
2110
				// We get here if vary-revision is set. This means that this page references
2111
				// itself (such as via self-transclusion). In this case, we need to make sure
2112
				// that any such self-references refer to the newly-saved revision, and not
2113
				// to the previous one, which could otherwise happen due to slave lag.
2114
				$oldCallback = $edit->popts->getCurrentRevisionCallback();
2115
				$edit->popts->setCurrentRevisionCallback(
2116
					function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2117
						if ( $title->equals( $revision->getTitle() ) ) {
2118
							return $revision;
2119
						} else {
2120
							return call_user_func( $oldCallback, $title, $parser );
2121
						}
2122
					}
2123
				);
2124
			} else {
2125
				// Try to avoid a second parse if {{REVISIONID}} is used
2126
				$edit->popts->setSpeculativeRevIdCallback( function () {
2127
					return 1 + (int)wfGetDB( DB_MASTER )->selectField(
2128
						'revision',
2129
						'MAX(rev_id)',
2130
						[],
2131
						__METHOD__
2132
					);
2133
				} );
2134
			}
2135
			$edit->output = $edit->pstContent
2136
				? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2137
				: null;
2138
		}
2139
2140
		$edit->newContent = $content;
2141
		$edit->oldContent = $this->getContent( Revision::RAW );
2142
2143
		// NOTE: B/C for hooks! don't use these fields!
2144
		$edit->newText = $edit->newContent
2145
			? ContentHandler::getContentText( $edit->newContent )
2146
			: '';
2147
		$edit->oldText = $edit->oldContent
2148
			? ContentHandler::getContentText( $edit->oldContent )
2149
			: '';
2150
		$edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2151
2152
		if ( $edit->output ) {
2153
			$edit->output->setCacheTime( wfTimestampNow() );
2154
		}
2155
2156
		// Process cache the result
2157
		$this->mPreparedEdit = $edit;
2158
2159
		return $edit;
2160
	}
2161
2162
	/**
2163
	 * Do standard deferred updates after page edit.
2164
	 * Update links tables, site stats, search index and message cache.
2165
	 * Purges pages that include this page if the text was changed here.
2166
	 * Every 100th edit, prune the recent changes table.
2167
	 *
2168
	 * @param Revision $revision
2169
	 * @param User $user User object that did the revision
2170
	 * @param array $options Array of options, following indexes are used:
2171
	 * - changed: boolean, whether the revision changed the content (default true)
2172
	 * - created: boolean, whether the revision created the page (default false)
2173
	 * - moved: boolean, whether the page was moved (default false)
2174
	 * - restored: boolean, whether the page was undeleted (default false)
2175
	 * - oldrevision: Revision object for the pre-update revision (default null)
2176
	 * - oldcountable: boolean, null, or string 'no-change' (default null):
2177
	 *   - boolean: whether the page was counted as an article before that
2178
	 *     revision, only used in changed is true and created is false
2179
	 *   - null: if created is false, don't update the article count; if created
2180
	 *     is true, do update the article count
2181
	 *   - 'no-change': don't update the article count, ever
2182
	 */
2183
	public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2184
		global $wgRCWatchCategoryMembership, $wgContLang;
2185
2186
		$options += [
2187
			'changed' => true,
2188
			'created' => false,
2189
			'moved' => false,
2190
			'restored' => false,
2191
			'oldrevision' => null,
2192
			'oldcountable' => null
2193
		];
2194
		$content = $revision->getContent();
2195
2196
		$logger = LoggerFactory::getInstance( 'SaveParse' );
2197
2198
		// See if the parser output before $revision was inserted is still valid
2199
		$editInfo = false;
2200
		if ( !$this->mPreparedEdit ) {
2201
			$logger->debug( __METHOD__ . ": No prepared edit...\n" );
2202
		} elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2203
			$logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
2204
		} elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
2205
			&& $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
2206
		) {
2207
			$logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
2208
		} elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
2209
			$logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
2210
		} else {
2211
			wfDebug( __METHOD__ . ": Using prepared edit...\n" );
2212
			$editInfo = $this->mPreparedEdit;
2213
		}
2214
2215
		if ( !$editInfo ) {
2216
			// Parse the text again if needed. Be careful not to do pre-save transform twice:
2217
			// $text is usually already pre-save transformed once. Avoid using the edit stash
2218
			// as any prepared content from there or in doEditContent() was already rejected.
2219
			$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 2194 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...
2220
		}
2221
2222
		// Save it to the parser cache.
2223
		// Make sure the cache time matches page_touched to avoid double parsing.
2224
		ParserCache::singleton()->save(
2225
			$editInfo->output, $this, $editInfo->popts,
2226
			$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...
2227
		);
2228
2229
		// Update the links tables and other secondary data
2230
		if ( $content ) {
2231
			$recursive = $options['changed']; // bug 50785
2232
			$updates = $content->getSecondaryDataUpdates(
2233
				$this->getTitle(), null, $recursive, $editInfo->output
2234
			);
2235
			foreach ( $updates as $update ) {
2236
				if ( $update instanceof LinksUpdate ) {
2237
					$update->setRevision( $revision );
2238
					$update->setTriggeringUser( $user );
2239
				}
2240
				DeferredUpdates::addUpdate( $update );
2241
			}
2242
			if ( $wgRCWatchCategoryMembership
2243
				&& $this->getContentHandler()->supportsCategories() === true
2244
				&& ( $options['changed'] || $options['created'] )
2245
				&& !$options['restored']
2246
			) {
2247
				// Note: jobs are pushed after deferred updates, so the job should be able to see
2248
				// the recent change entry (also done via deferred updates) and carry over any
2249
				// bot/deletion/IP flags, ect.
2250
				JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
2251
					$this->getTitle(),
2252
					[
2253
						'pageId' => $this->getId(),
2254
						'revTimestamp' => $revision->getTimestamp()
2255
					]
2256
				) );
2257
			}
2258
		}
2259
2260
		Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
2261
2262
		if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
2263
			// Flush old entries from the `recentchanges` table
2264
			if ( mt_rand( 0, 9 ) == 0 ) {
2265
				JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
2266
			}
2267
		}
2268
2269
		if ( !$this->exists() ) {
2270
			return;
2271
		}
2272
2273
		$id = $this->getId();
2274
		$title = $this->mTitle->getPrefixedDBkey();
2275
		$shortTitle = $this->mTitle->getDBkey();
2276
2277
		if ( $options['oldcountable'] === 'no-change' ||
2278
			( !$options['changed'] && !$options['moved'] )
2279
		) {
2280
			$good = 0;
2281
		} elseif ( $options['created'] ) {
2282
			$good = (int)$this->isCountable( $editInfo );
2283
		} elseif ( $options['oldcountable'] !== null ) {
2284
			$good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2285
		} else {
2286
			$good = 0;
2287
		}
2288
		$edits = $options['changed'] ? 1 : 0;
2289
		$total = $options['created'] ? 1 : 0;
2290
2291
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2292
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2194 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...
2293
2294
		// If this is another user's talk page, update newtalk.
2295
		// Don't do this if $options['changed'] = false (null-edits) nor if
2296
		// it's a minor edit and the user doesn't want notifications for those.
2297
		if ( $options['changed']
2298
			&& $this->mTitle->getNamespace() == NS_USER_TALK
2299
			&& $shortTitle != $user->getTitleKey()
2300
			&& !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2301
		) {
2302
			$recipient = User::newFromName( $shortTitle, false );
2303
			if ( !$recipient ) {
2304
				wfDebug( __METHOD__ . ": invalid username\n" );
2305
			} else {
2306
				// Allow extensions to prevent user notification
2307
				// when a new message is added to their talk page
2308
				if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
2309
					if ( User::isIP( $shortTitle ) ) {
2310
						// An anonymous user
2311
						$recipient->setNewtalk( true, $revision );
2312
					} elseif ( $recipient->isLoggedIn() ) {
2313
						$recipient->setNewtalk( true, $revision );
2314
					} else {
2315
						wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2316
					}
2317
				}
2318
			}
2319
		}
2320
2321
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2322
			// XXX: could skip pseudo-messages like js/css here, based on content model.
2323
			$msgtext = $content ? $content->getWikitextForTransclusion() : null;
2324
			if ( $msgtext === false || $msgtext === null ) {
2325
				$msgtext = '';
2326
			}
2327
2328
			MessageCache::singleton()->replace( $shortTitle, $msgtext );
2329
2330
			if ( $wgContLang->hasVariants() ) {
2331
				$wgContLang->updateConversionTable( $this->mTitle );
2332
			}
2333
		}
2334
2335
		if ( $options['created'] ) {
2336
			self::onArticleCreate( $this->mTitle );
2337
		} elseif ( $options['changed'] ) { // bug 50785
2338
			self::onArticleEdit( $this->mTitle, $revision );
2339
		}
2340
	}
2341
2342
	/**
2343
	 * Edit an article without doing all that other stuff
2344
	 * The article must already exist; link tables etc
2345
	 * are not updated, caches are not flushed.
2346
	 *
2347
	 * @param Content $content Content submitted
2348
	 * @param User $user The relevant user
2349
	 * @param string $comment Comment submitted
2350
	 * @param bool $minor Whereas it's a minor modification
2351
	 * @param string $serialFormat Format for storing the content in the database
2352
	 */
2353
	public function doQuickEditContent(
2354
		Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
2355
	) {
2356
2357
		$serialized = $content->serialize( $serialFormat );
2358
2359
		$dbw = wfGetDB( DB_MASTER );
2360
		$revision = new Revision( [
2361
			'title'      => $this->getTitle(), // for determining the default content model
2362
			'page'       => $this->getId(),
2363
			'user_text'  => $user->getName(),
2364
			'user'       => $user->getId(),
2365
			'text'       => $serialized,
2366
			'length'     => $content->getSize(),
2367
			'comment'    => $comment,
2368
			'minor_edit' => $minor ? 1 : 0,
2369
		] ); // XXX: set the content object?
2370
		$revision->insertOn( $dbw );
2371
		$this->updateRevisionOn( $dbw, $revision );
2372
2373
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
2374
2375
	}
2376
2377
	/**
2378
	 * Update the article's restriction field, and leave a log entry.
2379
	 * This works for protection both existing and non-existing pages.
2380
	 *
2381
	 * @param array $limit Set of restriction keys
2382
	 * @param array $expiry Per restriction type expiration
2383
	 * @param int &$cascade Set to false if cascading protection isn't allowed.
2384
	 * @param string $reason
2385
	 * @param User $user The user updating the restrictions
2386
	 * @param string|string[] $tags Change tags to add to the pages and protection log entries
2387
	 *   ($user should be able to add the specified tags before this is called)
2388
	 * @return Status Status object; if action is taken, $status->value is the log_id of the
2389
	 *   protection log entry.
2390
	 */
2391
	public function doUpdateRestrictions( array $limit, array $expiry,
2392
		&$cascade, $reason, User $user, $tags = null
2393
	) {
2394
		global $wgCascadingRestrictionLevels, $wgContLang;
2395
2396
		if ( wfReadOnly() ) {
2397
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2398
		}
2399
2400
		$this->loadPageData( 'fromdbmaster' );
2401
		$restrictionTypes = $this->mTitle->getRestrictionTypes();
2402
		$id = $this->getId();
2403
2404
		if ( !$cascade ) {
2405
			$cascade = false;
2406
		}
2407
2408
		// Take this opportunity to purge out expired restrictions
2409
		Title::purgeExpiredRestrictions();
2410
2411
		// @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2412
		// we expect a single selection, but the schema allows otherwise.
2413
		$isProtected = false;
2414
		$protect = false;
2415
		$changed = false;
2416
2417
		$dbw = wfGetDB( DB_MASTER );
2418
2419
		foreach ( $restrictionTypes as $action ) {
2420
			if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2421
				$expiry[$action] = 'infinity';
2422
			}
2423
			if ( !isset( $limit[$action] ) ) {
2424
				$limit[$action] = '';
2425
			} elseif ( $limit[$action] != '' ) {
2426
				$protect = true;
2427
			}
2428
2429
			// Get current restrictions on $action
2430
			$current = implode( '', $this->mTitle->getRestrictions( $action ) );
2431
			if ( $current != '' ) {
2432
				$isProtected = true;
2433
			}
2434
2435
			if ( $limit[$action] != $current ) {
2436
				$changed = true;
2437
			} elseif ( $limit[$action] != '' ) {
2438
				// Only check expiry change if the action is actually being
2439
				// protected, since expiry does nothing on an not-protected
2440
				// action.
2441
				if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2442
					$changed = true;
2443
				}
2444
			}
2445
		}
2446
2447
		if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2448
			$changed = true;
2449
		}
2450
2451
		// If nothing has changed, do nothing
2452
		if ( !$changed ) {
2453
			return Status::newGood();
2454
		}
2455
2456
		if ( !$protect ) { // No protection at all means unprotection
2457
			$revCommentMsg = 'unprotectedarticle';
2458
			$logAction = 'unprotect';
2459
		} elseif ( $isProtected ) {
2460
			$revCommentMsg = 'modifiedarticleprotection';
2461
			$logAction = 'modify';
2462
		} else {
2463
			$revCommentMsg = 'protectedarticle';
2464
			$logAction = 'protect';
2465
		}
2466
2467
		// Truncate for whole multibyte characters
2468
		$reason = $wgContLang->truncate( $reason, 255 );
2469
2470
		$logRelationsValues = [];
2471
		$logRelationsField = null;
2472
		$logParamsDetails = [];
2473
2474
		// Null revision (used for change tag insertion)
2475
		$nullRevision = null;
2476
2477
		if ( $id ) { // Protection of existing page
2478
			if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
2479
				return Status::newGood();
2480
			}
2481
2482
			// Only certain restrictions can cascade...
2483
			$editrestriction = isset( $limit['edit'] )
2484
				? [ $limit['edit'] ]
2485
				: $this->mTitle->getRestrictions( 'edit' );
2486
			foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2487
				$editrestriction[$key] = 'editprotected'; // backwards compatibility
2488
			}
2489
			foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2490
				$editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2491
			}
2492
2493
			$cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2494
			foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2495
				$cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2496
			}
2497
			foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2498
				$cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2499
			}
2500
2501
			// The schema allows multiple restrictions
2502
			if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2503
				$cascade = false;
2504
			}
2505
2506
			// insert null revision to identify the page protection change as edit summary
2507
			$latest = $this->getLatest();
2508
			$nullRevision = $this->insertProtectNullRevision(
2509
				$revCommentMsg,
2510
				$limit,
2511
				$expiry,
2512
				$cascade,
2513
				$reason,
2514
				$user
2515
			);
2516
2517
			if ( $nullRevision === null ) {
2518
				return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2519
			}
2520
2521
			$logRelationsField = 'pr_id';
2522
2523
			// Update restrictions table
2524
			foreach ( $limit as $action => $restrictions ) {
2525
				$dbw->delete(
2526
					'page_restrictions',
2527
					[
2528
						'pr_page' => $id,
2529
						'pr_type' => $action
2530
					],
2531
					__METHOD__
2532
				);
2533
				if ( $restrictions != '' ) {
2534
					$cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2535
					$dbw->insert(
2536
						'page_restrictions',
2537
						[
2538
							'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2539
							'pr_page' => $id,
2540
							'pr_type' => $action,
2541
							'pr_level' => $restrictions,
2542
							'pr_cascade' => $cascadeValue,
2543
							'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2544
						],
2545
						__METHOD__
2546
					);
2547
					$logRelationsValues[] = $dbw->insertId();
2548
					$logParamsDetails[] = [
2549
						'type' => $action,
2550
						'level' => $restrictions,
2551
						'expiry' => $expiry[$action],
2552
						'cascade' => (bool)$cascadeValue,
2553
					];
2554
				}
2555
			}
2556
2557
			// Clear out legacy restriction fields
2558
			$dbw->update(
2559
				'page',
2560
				[ 'page_restrictions' => '' ],
2561
				[ 'page_id' => $id ],
2562
				__METHOD__
2563
			);
2564
2565
			Hooks::run( 'NewRevisionFromEditComplete',
2566
				[ $this, $nullRevision, $latest, $user ] );
2567
			Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
2568
		} else { // Protection of non-existing page (also known as "title protection")
2569
			// Cascade protection is meaningless in this case
2570
			$cascade = false;
2571
2572
			if ( $limit['create'] != '' ) {
2573
				$dbw->replace( 'protected_titles',
2574
					[ [ 'pt_namespace', 'pt_title' ] ],
2575
					[
2576
						'pt_namespace' => $this->mTitle->getNamespace(),
2577
						'pt_title' => $this->mTitle->getDBkey(),
2578
						'pt_create_perm' => $limit['create'],
2579
						'pt_timestamp' => $dbw->timestamp(),
2580
						'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2581
						'pt_user' => $user->getId(),
2582
						'pt_reason' => $reason,
2583
					], __METHOD__
2584
				);
2585
				$logParamsDetails[] = [
2586
					'type' => 'create',
2587
					'level' => $limit['create'],
2588
					'expiry' => $expiry['create'],
2589
				];
2590
			} else {
2591
				$dbw->delete( 'protected_titles',
2592
					[
2593
						'pt_namespace' => $this->mTitle->getNamespace(),
2594
						'pt_title' => $this->mTitle->getDBkey()
2595
					], __METHOD__
2596
				);
2597
			}
2598
		}
2599
2600
		$this->mTitle->flushRestrictions();
2601
		InfoAction::invalidateCache( $this->mTitle );
2602
2603
		if ( $logAction == 'unprotect' ) {
2604
			$params = [];
2605
		} else {
2606
			$protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2607
			$params = [
2608
				'4::description' => $protectDescriptionLog, // parameter for IRC
2609
				'5:bool:cascade' => $cascade,
2610
				'details' => $logParamsDetails, // parameter for localize and api
2611
			];
2612
		}
2613
2614
		// Update the protection log
2615
		$logEntry = new ManualLogEntry( 'protect', $logAction );
2616
		$logEntry->setTarget( $this->mTitle );
2617
		$logEntry->setComment( $reason );
2618
		$logEntry->setPerformer( $user );
2619
		$logEntry->setParameters( $params );
2620
		if ( !is_null( $nullRevision ) ) {
2621
			$logEntry->setAssociatedRevId( $nullRevision->getId() );
2622
		}
2623
		$logEntry->setTags( $tags );
0 ignored issues
show
Bug introduced by
It seems like $tags defined by parameter $tags on line 2392 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...
2624
		if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2625
			$logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2626
		}
2627
		$logId = $logEntry->insert();
2628
		$logEntry->publish( $logId );
2629
2630
		return Status::newGood( $logId );
2631
	}
2632
2633
	/**
2634
	 * Insert a new null revision for this page.
2635
	 *
2636
	 * @param string $revCommentMsg Comment message key for the revision
2637
	 * @param array $limit Set of restriction keys
2638
	 * @param array $expiry Per restriction type expiration
2639
	 * @param int $cascade Set to false if cascading protection isn't allowed.
2640
	 * @param string $reason
2641
	 * @param User|null $user
2642
	 * @return Revision|null Null on error
2643
	 */
2644
	public function insertProtectNullRevision( $revCommentMsg, array $limit,
2645
		array $expiry, $cascade, $reason, $user = null
2646
	) {
2647
		global $wgContLang;
2648
		$dbw = wfGetDB( DB_MASTER );
2649
2650
		// Prepare a null revision to be added to the history
2651
		$editComment = $wgContLang->ucfirst(
2652
			wfMessage(
2653
				$revCommentMsg,
2654
				$this->mTitle->getPrefixedText()
2655
			)->inContentLanguage()->text()
2656
		);
2657
		if ( $reason ) {
2658
			$editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2659
		}
2660
		$protectDescription = $this->protectDescription( $limit, $expiry );
2661
		if ( $protectDescription ) {
2662
			$editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2663
			$editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2664
				->inContentLanguage()->text();
2665
		}
2666
		if ( $cascade ) {
2667
			$editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2668
			$editComment .= wfMessage( 'brackets' )->params(
2669
				wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2670
			)->inContentLanguage()->text();
2671
		}
2672
2673
		$nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2674
		if ( $nullRev ) {
2675
			$nullRev->insertOn( $dbw );
2676
2677
			// Update page record and touch page
2678
			$oldLatest = $nullRev->getParentId();
2679
			$this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2680
		}
2681
2682
		return $nullRev;
2683
	}
2684
2685
	/**
2686
	 * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid
2687
	 * @return string
2688
	 */
2689
	protected function formatExpiry( $expiry ) {
2690
		global $wgContLang;
2691
2692
		if ( $expiry != 'infinity' ) {
2693
			return wfMessage(
2694
				'protect-expiring',
2695
				$wgContLang->timeanddate( $expiry, false, false ),
2696
				$wgContLang->date( $expiry, false, false ),
2697
				$wgContLang->time( $expiry, false, false )
2698
			)->inContentLanguage()->text();
2699
		} else {
2700
			return wfMessage( 'protect-expiry-indefinite' )
2701
				->inContentLanguage()->text();
2702
		}
2703
	}
2704
2705
	/**
2706
	 * Builds the description to serve as comment for the edit.
2707
	 *
2708
	 * @param array $limit Set of restriction keys
2709
	 * @param array $expiry Per restriction type expiration
2710
	 * @return string
2711
	 */
2712
	public function protectDescription( array $limit, array $expiry ) {
2713
		$protectDescription = '';
2714
2715
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2716
			# $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
2717
			# All possible message keys are listed here for easier grepping:
2718
			# * restriction-create
2719
			# * restriction-edit
2720
			# * restriction-move
2721
			# * restriction-upload
2722
			$actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2723
			# $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
2724
			# with '' filtered out. All possible message keys are listed below:
2725
			# * protect-level-autoconfirmed
2726
			# * protect-level-sysop
2727
			$restrictionsText = wfMessage( 'protect-level-' . $restrictions )
2728
				->inContentLanguage()->text();
2729
2730
			$expiryText = $this->formatExpiry( $expiry[$action] );
2731
2732
			if ( $protectDescription !== '' ) {
2733
				$protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2734
			}
2735
			$protectDescription .= wfMessage( 'protect-summary-desc' )
2736
				->params( $actionText, $restrictionsText, $expiryText )
2737
				->inContentLanguage()->text();
2738
		}
2739
2740
		return $protectDescription;
2741
	}
2742
2743
	/**
2744
	 * Builds the description to serve as comment for the log entry.
2745
	 *
2746
	 * Some bots may parse IRC lines, which are generated from log entries which contain plain
2747
	 * protect description text. Keep them in old format to avoid breaking compatibility.
2748
	 * TODO: Fix protection log to store structured description and format it on-the-fly.
2749
	 *
2750
	 * @param array $limit Set of restriction keys
2751
	 * @param array $expiry Per restriction type expiration
2752
	 * @return string
2753
	 */
2754
	public function protectDescriptionLog( array $limit, array $expiry ) {
2755
		global $wgContLang;
2756
2757
		$protectDescriptionLog = '';
2758
2759
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2760
			$expiryText = $this->formatExpiry( $expiry[$action] );
2761
			$protectDescriptionLog .= $wgContLang->getDirMark() .
2762
				"[$action=$restrictions] ($expiryText)";
2763
		}
2764
2765
		return trim( $protectDescriptionLog );
2766
	}
2767
2768
	/**
2769
	 * Take an array of page restrictions and flatten it to a string
2770
	 * suitable for insertion into the page_restrictions field.
2771
	 *
2772
	 * @param string[] $limit
2773
	 *
2774
	 * @throws MWException
2775
	 * @return string
2776
	 */
2777
	protected static function flattenRestrictions( $limit ) {
2778
		if ( !is_array( $limit ) ) {
2779
			throw new MWException( __METHOD__ . ' given non-array restriction set' );
2780
		}
2781
2782
		$bits = [];
2783
		ksort( $limit );
2784
2785
		foreach ( array_filter( $limit ) as $action => $restrictions ) {
2786
			$bits[] = "$action=$restrictions";
2787
		}
2788
2789
		return implode( ':', $bits );
2790
	}
2791
2792
	/**
2793
	 * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
2794
	 * backwards compatibility, if you care about error reporting you should use
2795
	 * doDeleteArticleReal() instead.
2796
	 *
2797
	 * Deletes the article with database consistency, writes logs, purges caches
2798
	 *
2799
	 * @param string $reason Delete reason for deletion log
2800
	 * @param bool $suppress Suppress all revisions and log the deletion in
2801
	 *        the suppression log instead of the deletion log
2802
	 * @param int $u1 Unused
2803
	 * @param bool $u2 Unused
2804
	 * @param array|string &$error Array of errors to append to
2805
	 * @param User $user The deleting user
2806
	 * @return bool True if successful
2807
	 */
2808
	public function doDeleteArticle(
2809
		$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2810
	) {
2811
		$status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
2812
		return $status->isGood();
2813
	}
2814
2815
	/**
2816
	 * Back-end article deletion
2817
	 * Deletes the article with database consistency, writes logs, purges caches
2818
	 *
2819
	 * @since 1.19
2820
	 *
2821
	 * @param string $reason Delete reason for deletion log
2822
	 * @param bool $suppress Suppress all revisions and log the deletion in
2823
	 *   the suppression log instead of the deletion log
2824
	 * @param int $u1 Unused
2825
	 * @param bool $u2 Unused
2826
	 * @param array|string &$error Array of errors to append to
2827
	 * @param User $user The deleting user
2828
	 * @return Status Status object; if successful, $status->value is the log_id of the
2829
	 *   deletion log entry. If the page couldn't be deleted because it wasn't
2830
	 *   found, $status is a non-fatal 'cannotdelete' error
2831
	 */
2832
	public function doDeleteArticleReal(
2833
		$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
2834
	) {
2835
		global $wgUser, $wgContentHandlerUseDB;
2836
2837
		wfDebug( __METHOD__ . "\n" );
2838
2839
		$status = Status::newGood();
2840
2841
		if ( $this->mTitle->getDBkey() === '' ) {
2842
			$status->error( 'cannotdelete',
2843
				wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2844
			return $status;
2845
		}
2846
2847
		$user = is_null( $user ) ? $wgUser : $user;
2848
		if ( !Hooks::run( 'ArticleDelete',
2849
			[ &$this, &$user, &$reason, &$error, &$status, $suppress ]
2850
		) ) {
2851
			if ( $status->isOK() ) {
2852
				// Hook aborted but didn't set a fatal status
2853
				$status->fatal( 'delete-hook-aborted' );
2854
			}
2855
			return $status;
2856
		}
2857
2858
		$dbw = wfGetDB( DB_MASTER );
2859
		$dbw->startAtomic( __METHOD__ );
2860
2861
		$this->loadPageData( self::READ_LATEST );
2862
		$id = $this->getId();
2863
		// T98706: lock the page from various other updates but avoid using
2864
		// WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
2865
		// the revisions queries (which also JOIN on user). Only lock the page
2866
		// row and CAS check on page_latest to see if the trx snapshot matches.
2867
		$lockedLatest = $this->lockAndGetLatest();
2868
		if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
2869
			$dbw->endAtomic( __METHOD__ );
2870
			// Page not there or trx snapshot is stale
2871
			$status->error( 'cannotdelete',
2872
				wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2873
			return $status;
2874
		}
2875
2876
		// Given the lock above, we can be confident in the title and page ID values
2877
		$namespace = $this->getTitle()->getNamespace();
2878
		$dbKey = $this->getTitle()->getDBkey();
2879
2880
		// At this point we are now comitted to returning an OK
2881
		// status unless some DB query error or other exception comes up.
2882
		// This way callers don't have to call rollback() if $status is bad
2883
		// unless they actually try to catch exceptions (which is rare).
2884
2885
		// we need to remember the old content so we can use it to generate all deletion updates.
2886
		try {
2887
			$content = $this->getContent( Revision::RAW );
2888
		} catch ( Exception $ex ) {
2889
			wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
2890
				. $ex->getMessage() );
2891
2892
			$content = null;
2893
		}
2894
2895
		// Bitfields to further suppress the content
2896 View Code Duplication
		if ( $suppress ) {
2897
			$bitfield = 0;
2898
			// This should be 15...
2899
			$bitfield |= Revision::DELETED_TEXT;
2900
			$bitfield |= Revision::DELETED_COMMENT;
2901
			$bitfield |= Revision::DELETED_USER;
2902
			$bitfield |= Revision::DELETED_RESTRICTED;
2903
		} else {
2904
			$bitfield = 'rev_deleted';
2905
		}
2906
2907
		// For now, shunt the revision data into the archive table.
2908
		// Text is *not* removed from the text table; bulk storage
2909
		// is left intact to avoid breaking block-compression or
2910
		// immutable storage schemes.
2911
		// In the future, we may keep revisions and mark them with
2912
		// the rev_deleted field, which is reserved for this purpose.
2913
2914
		// Get all of the page revisions
2915
		$res = $dbw->select(
2916
			'revision',
2917
			Revision::selectFields(),
2918
			[ 'rev_page' => $id ],
2919
			__METHOD__,
2920
			'FOR UPDATE'
2921
		);
2922
		// Build their equivalent archive rows
2923
		$rowsInsert = [];
2924
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
2925
			$rowInsert = [
2926
				'ar_namespace'  => $namespace,
2927
				'ar_title'      => $dbKey,
2928
				'ar_comment'    => $row->rev_comment,
2929
				'ar_user'       => $row->rev_user,
2930
				'ar_user_text'  => $row->rev_user_text,
2931
				'ar_timestamp'  => $row->rev_timestamp,
2932
				'ar_minor_edit' => $row->rev_minor_edit,
2933
				'ar_rev_id'     => $row->rev_id,
2934
				'ar_parent_id'  => $row->rev_parent_id,
2935
				'ar_text_id'    => $row->rev_text_id,
2936
				'ar_text'       => '',
2937
				'ar_flags'      => '',
2938
				'ar_len'        => $row->rev_len,
2939
				'ar_page_id'    => $id,
2940
				'ar_deleted'    => $bitfield,
2941
				'ar_sha1'       => $row->rev_sha1,
2942
			];
2943
			if ( $wgContentHandlerUseDB ) {
2944
				$rowInsert['ar_content_model'] = $row->rev_content_model;
2945
				$rowInsert['ar_content_format'] = $row->rev_content_format;
2946
			}
2947
			$rowsInsert[] = $rowInsert;
2948
		}
2949
		// Copy them into the archive table
2950
		$dbw->insert( 'archive', $rowsInsert, __METHOD__ );
2951
		// Save this so we can pass it to the ArticleDeleteComplete hook.
2952
		$archivedRevisionCount = $dbw->affectedRows();
2953
2954
		// Clone the title and wikiPage, so we have the information we need when
2955
		// we log and run the ArticleDeleteComplete hook.
2956
		$logTitle = clone $this->mTitle;
2957
		$wikiPageBeforeDelete = clone $this;
2958
2959
		// Now that it's safely backed up, delete it
2960
		$dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
2961
2962
		if ( !$dbw->cascadingDeletes() ) {
2963
			$dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
2964
		}
2965
2966
		// Log the deletion, if the page was suppressed, put it in the suppression log instead
2967
		$logtype = $suppress ? 'suppress' : 'delete';
2968
2969
		$logEntry = new ManualLogEntry( $logtype, 'delete' );
2970
		$logEntry->setPerformer( $user );
2971
		$logEntry->setTarget( $logTitle );
2972
		$logEntry->setComment( $reason );
2973
		$logid = $logEntry->insert();
2974
2975
		$dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
2976
			// Bug 56776: avoid deadlocks (especially from FileDeleteForm)
2977
			$logEntry->publish( $logid );
2978
		} );
2979
2980
		$dbw->endAtomic( __METHOD__ );
2981
2982
		$this->doDeleteUpdates( $id, $content );
2983
2984
		Hooks::run( 'ArticleDeleteComplete', [
2985
			&$wikiPageBeforeDelete,
2986
			&$user,
2987
			$reason,
2988
			$id,
2989
			$content,
2990
			$logEntry,
2991
			$archivedRevisionCount
2992
		] );
2993
		$status->value = $logid;
2994
2995
		// Show log excerpt on 404 pages rather than just a link
2996
		$cache = ObjectCache::getMainStashInstance();
2997
		$key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2998
		$cache->set( $key, 1, $cache::TTL_DAY );
2999
3000
		return $status;
3001
	}
3002
3003
	/**
3004
	 * Lock the page row for this title+id and return page_latest (or 0)
3005
	 *
3006
	 * @return integer Returns 0 if no row was found with this title+id
3007
	 * @since 1.27
3008
	 */
3009
	public function lockAndGetLatest() {
3010
		return (int)wfGetDB( DB_MASTER )->selectField(
3011
			'page',
3012
			'page_latest',
3013
			[
3014
				'page_id' => $this->getId(),
3015
				// Typically page_id is enough, but some code might try to do
3016
				// updates assuming the title is the same, so verify that
3017
				'page_namespace' => $this->getTitle()->getNamespace(),
3018
				'page_title' => $this->getTitle()->getDBkey()
3019
			],
3020
			__METHOD__,
3021
			[ 'FOR UPDATE' ]
3022
		);
3023
	}
3024
3025
	/**
3026
	 * Do some database updates after deletion
3027
	 *
3028
	 * @param int $id The page_id value of the page being deleted
3029
	 * @param Content $content Optional page content to be used when determining
3030
	 *   the required updates. This may be needed because $this->getContent()
3031
	 *   may already return null when the page proper was deleted.
3032
	 */
3033
	public function doDeleteUpdates( $id, Content $content = null ) {
3034
		try {
3035
			$countable = $this->isCountable();
3036
		} catch ( Exception $ex ) {
3037
			// fallback for deleting broken pages for which we cannot load the content for
3038
			// some reason. Note that doDeleteArticleReal() already logged this problem.
3039
			$countable = false;
3040
		}
3041
3042
		// Update site status
3043
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
3044
3045
		// Delete pagelinks, update secondary indexes, etc
3046
		$updates = $this->getDeletionUpdates( $content );
3047
		foreach ( $updates as $update ) {
3048
			DeferredUpdates::addUpdate( $update );
3049
		}
3050
3051
		// Reparse any pages transcluding this page
3052
		LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
3053
3054
		// Reparse any pages including this image
3055
		if ( $this->mTitle->getNamespace() == NS_FILE ) {
3056
			LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
3057
		}
3058
3059
		// Clear caches
3060
		WikiPage::onArticleDelete( $this->mTitle );
3061
3062
		// Reset this object and the Title object
3063
		$this->loadFromRow( false, self::READ_LATEST );
3064
3065
		// Search engine
3066
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
3067
	}
3068
3069
	/**
3070
	 * Roll back the most recent consecutive set of edits to a page
3071
	 * from the same user; fails if there are no eligible edits to
3072
	 * roll back to, e.g. user is the sole contributor. This function
3073
	 * performs permissions checks on $user, then calls commitRollback()
3074
	 * to do the dirty work
3075
	 *
3076
	 * @todo Separate the business/permission stuff out from backend code
3077
	 * @todo Remove $token parameter. Already verified by RollbackAction and ApiRollback.
3078
	 *
3079
	 * @param string $fromP Name of the user whose edits to rollback.
3080
	 * @param string $summary Custom summary. Set to default summary if empty.
3081
	 * @param string $token Rollback token.
3082
	 * @param bool $bot If true, mark all reverted edits as bot.
3083
	 *
3084
	 * @param array $resultDetails Array contains result-specific array of additional values
3085
	 *    'alreadyrolled' : 'current' (rev)
3086
	 *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
3087
	 *
3088
	 * @param User $user The user performing the rollback
3089
	 * @param array|null $tags Change tags to apply to the rollback
3090
	 * Callers are responsible for permission checks
3091
	 * (with ChangeTags::canAddTagsAccompanyingChange)
3092
	 *
3093
	 * @return array Array of errors, each error formatted as
3094
	 *   array(messagekey, param1, param2, ...).
3095
	 * On success, the array is empty.  This array can also be passed to
3096
	 * OutputPage::showPermissionsErrorPage().
3097
	 */
3098
	public function doRollback(
3099
		$fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
3100
	) {
3101
		$resultDetails = null;
3102
3103
		// Check permissions
3104
		$editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
3105
		$rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
3106
		$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3107
3108
		if ( !$user->matchEditToken( $token, 'rollback' ) ) {
3109
			$errors[] = [ 'sessionfailure' ];
3110
		}
3111
3112
		if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
3113
			$errors[] = [ 'actionthrottledtext' ];
3114
		}
3115
3116
		// If there were errors, bail out now
3117
		if ( !empty( $errors ) ) {
3118
			return $errors;
3119
		}
3120
3121
		return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
3122
	}
3123
3124
	/**
3125
	 * Backend implementation of doRollback(), please refer there for parameter
3126
	 * and return value documentation
3127
	 *
3128
	 * NOTE: This function does NOT check ANY permissions, it just commits the
3129
	 * rollback to the DB. Therefore, you should only call this function direct-
3130
	 * ly if you want to use custom permissions checks. If you don't, use
3131
	 * doRollback() instead.
3132
	 * @param string $fromP Name of the user whose edits to rollback.
3133
	 * @param string $summary Custom summary. Set to default summary if empty.
3134
	 * @param bool $bot If true, mark all reverted edits as bot.
3135
	 *
3136
	 * @param array $resultDetails Contains result-specific array of additional values
3137
	 * @param User $guser The user performing the rollback
3138
	 * @param array|null $tags Change tags to apply to the rollback
3139
	 * Callers are responsible for permission checks
3140
	 * (with ChangeTags::canAddTagsAccompanyingChange)
3141
	 *
3142
	 * @return array
3143
	 */
3144
	public function commitRollback( $fromP, $summary, $bot,
3145
		&$resultDetails, User $guser, $tags = null
3146
	) {
3147
		global $wgUseRCPatrol, $wgContLang;
3148
3149
		$dbw = wfGetDB( DB_MASTER );
3150
3151
		if ( wfReadOnly() ) {
3152
			return [ [ 'readonlytext' ] ];
3153
		}
3154
3155
		// Get the last editor
3156
		$current = $this->getRevision();
3157
		if ( is_null( $current ) ) {
3158
			// Something wrong... no page?
3159
			return [ [ 'notanarticle' ] ];
3160
		}
3161
3162
		$from = str_replace( '_', ' ', $fromP );
3163
		// User name given should match up with the top revision.
3164
		// If the user was deleted then $from should be empty.
3165 View Code Duplication
		if ( $from != $current->getUserText() ) {
3166
			$resultDetails = [ 'current' => $current ];
3167
			return [ [ 'alreadyrolled',
3168
				htmlspecialchars( $this->mTitle->getPrefixedText() ),
3169
				htmlspecialchars( $fromP ),
3170
				htmlspecialchars( $current->getUserText() )
3171
			] ];
3172
		}
3173
3174
		// Get the last edit not by this person...
3175
		// Note: these may not be public values
3176
		$user = intval( $current->getUser( Revision::RAW ) );
3177
		$user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
0 ignored issues
show
Bug introduced by
It seems like $current->getUserText(\Revision::RAW) targeting Revision::getUserText() can also be of type boolean; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, 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...
3178
		$s = $dbw->selectRow( 'revision',
3179
			[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
3180
			[ 'rev_page' => $current->getPage(),
3181
				"rev_user != {$user} OR rev_user_text != {$user_text}"
3182
			], __METHOD__,
3183
			[ 'USE INDEX' => 'page_timestamp',
3184
				'ORDER BY' => 'rev_timestamp DESC' ]
3185
			);
3186
		if ( $s === false ) {
3187
			// No one else ever edited this page
3188
			return [ [ 'cantrollback' ] ];
3189
		} elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3190
			|| $s->rev_deleted & Revision::DELETED_USER
3191
		) {
3192
			// Only admins can see this text
3193
			return [ [ 'notvisiblerev' ] ];
3194
		}
3195
3196
		// Generate the edit summary if necessary
3197
		$target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
3198
		if ( empty( $summary ) ) {
3199
			if ( $from == '' ) { // no public user name
3200
				$summary = wfMessage( 'revertpage-nouser' );
3201
			} else {
3202
				$summary = wfMessage( 'revertpage' );
3203
			}
3204
		}
3205
3206
		// Allow the custom summary to use the same args as the default message
3207
		$args = [
3208
			$target->getUserText(), $from, $s->rev_id,
3209
			$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3210
			$current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3211
		];
3212
		if ( $summary instanceof Message ) {
3213
			$summary = $summary->params( $args )->inContentLanguage()->text();
3214
		} else {
3215
			$summary = wfMsgReplaceArgs( $summary, $args );
3216
		}
3217
3218
		// Trim spaces on user supplied text
3219
		$summary = trim( $summary );
3220
3221
		// Truncate for whole multibyte characters.
3222
		$summary = $wgContLang->truncate( $summary, 255 );
3223
3224
		// Save
3225
		$flags = EDIT_UPDATE | EDIT_INTERNAL;
3226
3227
		if ( $guser->isAllowed( 'minoredit' ) ) {
3228
			$flags |= EDIT_MINOR;
3229
		}
3230
3231
		if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3232
			$flags |= EDIT_FORCE_BOT;
3233
		}
3234
3235
		// Actually store the edit
3236
		$status = $this->doEditContent(
3237
			$target->getContent(),
3238
			$summary,
3239
			$flags,
3240
			$target->getId(),
3241
			$guser,
3242
			null,
3243
			$tags
3244
		);
3245
3246
		// Set patrolling and bot flag on the edits, which gets rollbacked.
3247
		// This is done even on edit failure to have patrolling in that case (bug 62157).
3248
		$set = [];
3249
		if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3250
			// Mark all reverted edits as bot
3251
			$set['rc_bot'] = 1;
3252
		}
3253
3254
		if ( $wgUseRCPatrol ) {
3255
			// Mark all reverted edits as patrolled
3256
			$set['rc_patrolled'] = 1;
3257
		}
3258
3259
		if ( count( $set ) ) {
3260
			$dbw->update( 'recentchanges', $set,
3261
				[ /* WHERE */
3262
					'rc_cur_id' => $current->getPage(),
3263
					'rc_user_text' => $current->getUserText(),
3264
					'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3265
				],
3266
				__METHOD__
3267
			);
3268
		}
3269
3270
		if ( !$status->isOK() ) {
3271
			return $status->getErrorsArray();
3272
		}
3273
3274
		// raise error, when the edit is an edit without a new version
3275
		$statusRev = isset( $status->value['revision'] )
3276
			? $status->value['revision']
3277
			: null;
3278 View Code Duplication
		if ( !( $statusRev instanceof Revision ) ) {
3279
			$resultDetails = [ 'current' => $current ];
3280
			return [ [ 'alreadyrolled',
3281
					htmlspecialchars( $this->mTitle->getPrefixedText() ),
3282
					htmlspecialchars( $fromP ),
3283
					htmlspecialchars( $current->getUserText() )
3284
			] ];
3285
		}
3286
3287
		$revId = $statusRev->getId();
3288
3289
		Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3290
3291
		$resultDetails = [
3292
			'summary' => $summary,
3293
			'current' => $current,
3294
			'target' => $target,
3295
			'newid' => $revId
3296
		];
3297
3298
		return [];
3299
	}
3300
3301
	/**
3302
	 * The onArticle*() functions are supposed to be a kind of hooks
3303
	 * which should be called whenever any of the specified actions
3304
	 * are done.
3305
	 *
3306
	 * This is a good place to put code to clear caches, for instance.
3307
	 *
3308
	 * This is called on page move and undelete, as well as edit
3309
	 *
3310
	 * @param Title $title
3311
	 */
3312
	public static function onArticleCreate( Title $title ) {
3313
		// Update existence markers on article/talk tabs...
3314
		$other = $title->getOtherPage();
3315
3316
		$other->purgeSquid();
3317
3318
		$title->touchLinks();
3319
		$title->purgeSquid();
3320
		$title->deleteTitleProtection();
3321
3322
		if ( $title->getNamespace() == NS_CATEGORY ) {
3323
			// Load the Category object, which will schedule a job to create
3324
			// the category table row if necessary. Checking a slave is ok
3325
			// here, in the worst case it'll run an unnecessary recount job on
3326
			// a category that probably doesn't have many members.
3327
			Category::newFromTitle( $title )->getID();
3328
		}
3329
	}
3330
3331
	/**
3332
	 * Clears caches when article is deleted
3333
	 *
3334
	 * @param Title $title
3335
	 */
3336
	public static function onArticleDelete( Title $title ) {
3337
		global $wgContLang;
3338
3339
		// Update existence markers on article/talk tabs...
3340
		$other = $title->getOtherPage();
3341
3342
		$other->purgeSquid();
3343
3344
		$title->touchLinks();
3345
		$title->purgeSquid();
3346
3347
		// File cache
3348
		HTMLFileCache::clearFileCache( $title );
3349
		InfoAction::invalidateCache( $title );
3350
3351
		// Messages
3352
		if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3353
			MessageCache::singleton()->replace( $title->getDBkey(), false );
3354
3355
			if ( $wgContLang->hasVariants() ) {
3356
				$wgContLang->updateConversionTable( $title );
3357
			}
3358
		}
3359
3360
		// Images
3361
		if ( $title->getNamespace() == NS_FILE ) {
3362
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3363
		}
3364
3365
		// User talk pages
3366
		if ( $title->getNamespace() == NS_USER_TALK ) {
3367
			$user = User::newFromName( $title->getText(), false );
3368
			if ( $user ) {
3369
				$user->setNewtalk( false );
3370
			}
3371
		}
3372
3373
		// Image redirects
3374
		RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3375
	}
3376
3377
	/**
3378
	 * Purge caches on page update etc
3379
	 *
3380
	 * @param Title $title
3381
	 * @param Revision|null $revision Revision that was just saved, may be null
3382
	 */
3383
	public static function onArticleEdit( Title $title, Revision $revision = null ) {
3384
		// Invalidate caches of articles which include this page
3385
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3386
3387
		// Invalidate the caches of all pages which redirect here
3388
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3389
3390
		// Purge CDN for this page only
3391
		$title->purgeSquid();
3392
		// Clear file cache for this page only
3393
		HTMLFileCache::clearFileCache( $title );
3394
3395
		$revid = $revision ? $revision->getId() : null;
3396
		DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
3397
			InfoAction::invalidateCache( $title, $revid );
3398
		} );
3399
	}
3400
3401
	/**#@-*/
3402
3403
	/**
3404
	 * Returns a list of categories this page is a member of.
3405
	 * Results will include hidden categories
3406
	 *
3407
	 * @return TitleArray
3408
	 */
3409
	public function getCategories() {
3410
		$id = $this->getId();
3411
		if ( $id == 0 ) {
3412
			return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3413
		}
3414
3415
		$dbr = wfGetDB( DB_SLAVE );
3416
		$res = $dbr->select( 'categorylinks',
3417
			[ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3418
			// Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
3419
			// as not being aliases, and NS_CATEGORY is numeric
3420
			[ 'cl_from' => $id ],
3421
			__METHOD__ );
3422
3423
		return TitleArray::newFromResult( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('categoryli...m' => $id), __METHOD__) on line 3416 can also be of type boolean; however, TitleArray::newFromResult() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
3424
	}
3425
3426
	/**
3427
	 * Returns a list of hidden categories this page is a member of.
3428
	 * Uses the page_props and categorylinks tables.
3429
	 *
3430
	 * @return array Array of Title objects
3431
	 */
3432
	public function getHiddenCategories() {
3433
		$result = [];
3434
		$id = $this->getId();
3435
3436
		if ( $id == 0 ) {
3437
			return [];
3438
		}
3439
3440
		$dbr = wfGetDB( DB_SLAVE );
3441
		$res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3442
			[ 'cl_to' ],
3443
			[ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3444
				'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3445
			__METHOD__ );
3446
3447
		if ( $res !== false ) {
3448
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
3449
				$result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3450
			}
3451
		}
3452
3453
		return $result;
3454
	}
3455
3456
	/**
3457
	 * Return an applicable autosummary if one exists for the given edit.
3458
	 * @param string|null $oldtext The previous text of the page.
3459
	 * @param string|null $newtext The submitted text of the page.
3460
	 * @param int $flags Bitmask: a bitmask of flags submitted for the edit.
3461
	 * @return string An appropriate autosummary, or an empty string.
3462
	 *
3463
	 * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
3464
	 */
3465
	public static function getAutosummary( $oldtext, $newtext, $flags ) {
3466
		// NOTE: stub for backwards-compatibility. assumes the given text is
3467
		// wikitext. will break horribly if it isn't.
3468
3469
		ContentHandler::deprecated( __METHOD__, '1.21' );
3470
3471
		$handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
3472
		$oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3473
		$newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3474
3475
		return $handler->getAutosummary( $oldContent, $newContent, $flags );
3476
	}
3477
3478
	/**
3479
	 * Auto-generates a deletion reason
3480
	 *
3481
	 * @param bool &$hasHistory Whether the page has a history
3482
	 * @return string|bool String containing deletion reason or empty string, or boolean false
3483
	 *    if no revision occurred
3484
	 */
3485
	public function getAutoDeleteReason( &$hasHistory ) {
3486
		return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3487
	}
3488
3489
	/**
3490
	 * Update all the appropriate counts in the category table, given that
3491
	 * we've added the categories $added and deleted the categories $deleted.
3492
	 *
3493
	 * @param array $added The names of categories that were added
3494
	 * @param array $deleted The names of categories that were deleted
3495
	 * @param integer $id Page ID (this should be the original deleted page ID)
3496
	 */
3497
	public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
3498
		$id = $id ?: $this->getId();
3499
		$dbw = wfGetDB( DB_MASTER );
3500
		$method = __METHOD__;
3501
		// Do this at the end of the commit to reduce lock wait timeouts
3502
		$dbw->onTransactionPreCommitOrIdle(
3503
			function () use ( $dbw, $added, $deleted, $id, $method ) {
3504
				$ns = $this->getTitle()->getNamespace();
3505
3506
				$addFields = [ 'cat_pages = cat_pages + 1' ];
3507
				$removeFields = [ 'cat_pages = cat_pages - 1' ];
3508
				if ( $ns == NS_CATEGORY ) {
3509
					$addFields[] = 'cat_subcats = cat_subcats + 1';
3510
					$removeFields[] = 'cat_subcats = cat_subcats - 1';
3511
				} elseif ( $ns == NS_FILE ) {
3512
					$addFields[] = 'cat_files = cat_files + 1';
3513
					$removeFields[] = 'cat_files = cat_files - 1';
3514
				}
3515
3516
				if ( count( $added ) ) {
3517
					$existingAdded = $dbw->selectFieldValues(
3518
						'category',
3519
						'cat_title',
3520
						[ 'cat_title' => $added ],
3521
						$method
3522
					);
3523
3524
					// For category rows that already exist, do a plain
3525
					// UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3526
					// to avoid creating gaps in the cat_id sequence.
3527
					if ( count( $existingAdded ) ) {
3528
						$dbw->update(
3529
							'category',
3530
							$addFields,
3531
							[ 'cat_title' => $existingAdded ],
3532
							$method
3533
						);
3534
					}
3535
3536
					$missingAdded = array_diff( $added, $existingAdded );
3537
					if ( count( $missingAdded ) ) {
3538
						$insertRows = [];
3539
						foreach ( $missingAdded as $cat ) {
3540
							$insertRows[] = [
3541
								'cat_title'   => $cat,
3542
								'cat_pages'   => 1,
3543
								'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3544
								'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
3545
							];
3546
						}
3547
						$dbw->upsert(
3548
							'category',
3549
							$insertRows,
3550
							[ 'cat_title' ],
3551
							$addFields,
3552
							$method
3553
						);
3554
					}
3555
				}
3556
3557
				if ( count( $deleted ) ) {
3558
					$dbw->update(
3559
						'category',
3560
						$removeFields,
3561
						[ 'cat_title' => $deleted ],
3562
						$method
3563
					);
3564
				}
3565
3566
				foreach ( $added as $catName ) {
3567
					$cat = Category::newFromName( $catName );
3568
					Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
3569
				}
3570
3571
				foreach ( $deleted as $catName ) {
3572
					$cat = Category::newFromName( $catName );
3573
					Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3574
				}
3575
3576
				// Refresh counts on categories that should be empty now, to
3577
				// trigger possible deletion. Check master for the most
3578
				// up-to-date cat_pages.
3579
				if ( count( $deleted ) ) {
3580
					$rows = $dbw->select(
3581
						'category',
3582
						[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
3583
						[ 'cat_title' => $deleted, 'cat_pages <= 0' ],
3584
						$method
3585
					);
3586
					foreach ( $rows as $row ) {
0 ignored issues
show
Bug introduced by
The expression $rows of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
3587
						$cat = Category::newFromRow( $row );
3588
						$cat->refreshCounts();
3589
					}
3590
				}
3591
			}
3592
		);
3593
	}
3594
3595
	/**
3596
	 * Opportunistically enqueue link update jobs given fresh parser output if useful
3597
	 *
3598
	 * @param ParserOutput $parserOutput Current version page output
3599
	 * @since 1.25
3600
	 */
3601
	public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3602
		if ( wfReadOnly() ) {
3603
			return;
3604
		}
3605
3606
		if ( !Hooks::run( 'OpportunisticLinksUpdate',
3607
			[ $this, $this->mTitle, $parserOutput ]
3608
		) ) {
3609
			return;
3610
		}
3611
3612
		$config = RequestContext::getMain()->getConfig();
3613
3614
		$params = [
3615
			'isOpportunistic' => true,
3616
			'rootJobTimestamp' => $parserOutput->getCacheTime()
3617
		];
3618
3619
		if ( $this->mTitle->areRestrictionsCascading() ) {
3620
			// If the page is cascade protecting, the links should really be up-to-date
3621
			JobQueueGroup::singleton()->lazyPush(
3622
				RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3623
			);
3624
		} elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3625
			// Assume the output contains "dynamic" time/random based magic words.
3626
			// Only update pages that expired due to dynamic content and NOT due to edits
3627
			// to referenced templates/files. When the cache expires due to dynamic content,
3628
			// page_touched is unchanged. We want to avoid triggering redundant jobs due to
3629
			// views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3630
			// template/file edit already triggered recursive RefreshLinksJob jobs.
3631
			if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3632
				// If a page is uncacheable, do not keep spamming a job for it.
3633
				// Although it would be de-duplicated, it would still waste I/O.
3634
				$cache = ObjectCache::getLocalClusterInstance();
3635
				$key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3636
				$ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3637
				if ( $cache->add( $key, time(), $ttl ) ) {
3638
					JobQueueGroup::singleton()->lazyPush(
3639
						RefreshLinksJob::newDynamic( $this->mTitle, $params )
3640
					);
3641
				}
3642
			}
3643
		}
3644
	}
3645
3646
	/**
3647
	 * Returns a list of updates to be performed when this page is deleted. The
3648
	 * updates should remove any information about this page from secondary data
3649
	 * stores such as links tables.
3650
	 *
3651
	 * @param Content|null $content Optional Content object for determining the
3652
	 *   necessary updates.
3653
	 * @return DataUpdate[]
3654
	 */
3655
	public function getDeletionUpdates( Content $content = null ) {
3656
		if ( !$content ) {
3657
			// load content object, which may be used to determine the necessary updates.
3658
			// XXX: the content may not be needed to determine the updates.
3659
			try {
3660
				$content = $this->getContent( Revision::RAW );
3661
			} catch ( Exception $ex ) {
3662
				// If we can't load the content, something is wrong. Perhaps that's why
3663
				// the user is trying to delete the page, so let's not fail in that case.
3664
				// Note that doDeleteArticleReal() will already have logged an issue with
3665
				// loading the content.
3666
			}
3667
		}
3668
3669
		if ( !$content ) {
3670
			$updates = [];
3671
		} else {
3672
			$updates = $content->getDeletionUpdates( $this );
3673
		}
3674
3675
		Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3676
		return $updates;
3677
	}
3678
}
3679