Completed
Branch master (54277f)
by
unknown
24:54
created

WikiPage::isCountable()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 36
rs 6.7272
cc 7
eloc 18
nc 9
nop 1
1
<?php
2
/**
3
 * Base representation for a MediaWiki page.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * Class representing a MediaWiki article and history.
25
 *
26
 * Some fields are public only for backwards-compatibility. Use accessors.
27
 * In the past, this class was part of Article.php and everything was public.
28
 */
29
class WikiPage implements Page, IDBAccessObject {
30
	// Constants for $mDataLoadedFrom and related
31
32
	/**
33
	 * @var Title
34
	 */
35
	public $mTitle = null;
36
37
	/**@{{
38
	 * @protected
39
	 */
40
	public $mDataLoaded = false;         // !< Boolean
41
	public $mIsRedirect = false;         // !< Boolean
42
	public $mLatest = false;             // !< Integer (false means "not loaded")
43
	/**@}}*/
44
45
	/** @var stdClass Map of cache fields (text, parser output, ect) for a proposed/new edit */
46
	public $mPreparedEdit = false;
47
48
	/**
49
	 * @var int
50
	 */
51
	protected $mId = null;
52
53
	/**
54
	 * @var int One of the READ_* constants
55
	 */
56
	protected $mDataLoadedFrom = self::READ_NONE;
57
58
	/**
59
	 * @var Title
60
	 */
61
	protected $mRedirectTarget = null;
62
63
	/**
64
	 * @var Revision
65
	 */
66
	protected $mLastRevision = null;
67
68
	/**
69
	 * @var string Timestamp of the current revision or empty string if not loaded
70
	 */
71
	protected $mTimestamp = '';
72
73
	/**
74
	 * @var string
75
	 */
76
	protected $mTouched = '19700101000000';
77
78
	/**
79
	 * @var string
80
	 */
81
	protected $mLinksUpdated = '19700101000000';
82
83
	/**
84
	 * Constructor and clear the article
85
	 * @param Title $title Reference to a Title object.
86
	 */
87
	public function __construct( Title $title ) {
88
		$this->mTitle = $title;
89
	}
90
91
	/**
92
	 * Create a WikiPage object of the appropriate class for the given title.
93
	 *
94
	 * @param Title $title
95
	 *
96
	 * @throws MWException
97
	 * @return WikiPage Object of the appropriate type
98
	 */
99
	public static function factory( Title $title ) {
100
		$ns = $title->getNamespace();
101
102
		if ( $ns == NS_MEDIA ) {
103
			throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
104
		} elseif ( $ns < 0 ) {
105
			throw new MWException( "Invalid or virtual namespace $ns given." );
106
		}
107
108
		switch ( $ns ) {
109
			case NS_FILE:
110
				$page = new WikiFilePage( $title );
111
				break;
112
			case NS_CATEGORY:
113
				$page = new WikiCategoryPage( $title );
114
				break;
115
			default:
116
				$page = new WikiPage( $title );
117
		}
118
119
		return $page;
120
	}
121
122
	/**
123
	 * Constructor from a page id
124
	 *
125
	 * @param int $id Article ID to load
126
	 * @param string|int $from One of the following values:
127
	 *        - "fromdb" or WikiPage::READ_NORMAL to select from a slave database
128
	 *        - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
129
	 *
130
	 * @return WikiPage|null
131
	 */
132
	public static function newFromID( $id, $from = 'fromdb' ) {
133
		// page id's are never 0 or negative, see bug 61166
134
		if ( $id < 1 ) {
135
			return null;
136
		}
137
138
		$from = self::convertSelectType( $from );
139
		$db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
140
		$row = $db->selectRow(
141
			'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
142
		if ( !$row ) {
143
			return null;
144
		}
145
		return self::newFromRow( $row, $from );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $db->selectRow('page', s...d' => $id), __METHOD__) on line 140 can also be of type boolean; however, WikiPage::newFromRow() does only seem to accept object, maybe add an additional type check?

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

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

    return array();
}

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

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

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

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

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

    return array();
}

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

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

Loading history...
146
	}
147
148
	/**
149
	 * Constructor from a database row
150
	 *
151
	 * @since 1.20
152
	 * @param object $row Database row containing at least fields returned by selectFields().
153
	 * @param string|int $from Source of $data:
154
	 *        - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
155
	 *        - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
156
	 *        - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
157
	 * @return WikiPage
158
	 */
159
	public static function newFromRow( $row, $from = 'fromdb' ) {
160
		$page = self::factory( Title::newFromRow( $row ) );
161
		$page->loadFromRow( $row, $from );
162
		return $page;
163
	}
164
165
	/**
166
	 * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
167
	 *
168
	 * @param object|string|int $type
169
	 * @return mixed
170
	 */
171
	private static function convertSelectType( $type ) {
172
		switch ( $type ) {
173
		case 'fromdb':
174
			return self::READ_NORMAL;
175
		case 'fromdbmaster':
176
			return self::READ_LATEST;
177
		case 'forupdate':
178
			return self::READ_LOCKING;
179
		default:
180
			// It may already be an integer or whatever else
181
			return $type;
182
		}
183
	}
184
185
	/**
186
	 * Returns overrides for action handlers.
187
	 * Classes listed here will be used instead of the default one when
188
	 * (and only when) $wgActions[$action] === true. This allows subclasses
189
	 * to override the default behavior.
190
	 *
191
	 * @todo Move this UI stuff somewhere else
192
	 *
193
	 * @return array
194
	 */
195
	public function getActionOverrides() {
196
		$content_handler = $this->getContentHandler();
197
		return $content_handler->getActionOverrides();
198
	}
199
200
	/**
201
	 * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
202
	 *
203
	 * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
204
	 *
205
	 * @return ContentHandler
206
	 *
207
	 * @since 1.21
208
	 */
209
	public function getContentHandler() {
210
		return ContentHandler::getForModelID( $this->getContentModel() );
211
	}
212
213
	/**
214
	 * Get the title object of the article
215
	 * @return Title Title object of this page
216
	 */
217
	public function getTitle() {
218
		return $this->mTitle;
219
	}
220
221
	/**
222
	 * Clear the object
223
	 * @return void
224
	 */
225
	public function clear() {
226
		$this->mDataLoaded = false;
227
		$this->mDataLoadedFrom = self::READ_NONE;
228
229
		$this->clearCacheFields();
230
	}
231
232
	/**
233
	 * Clear the object cache fields
234
	 * @return void
235
	 */
236
	protected function clearCacheFields() {
237
		$this->mId = null;
238
		$this->mRedirectTarget = null; // Title object if set
239
		$this->mLastRevision = null; // Latest revision
240
		$this->mTouched = '19700101000000';
241
		$this->mLinksUpdated = '19700101000000';
242
		$this->mTimestamp = '';
243
		$this->mIsRedirect = false;
244
		$this->mLatest = false;
245
		// Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
246
		// the requested rev ID and content against the cached one for equality. For most
247
		// content types, the output should not change during the lifetime of this cache.
248
		// Clearing it can cause extra parses on edit for no reason.
249
	}
250
251
	/**
252
	 * Clear the mPreparedEdit cache field, as may be needed by mutable content types
253
	 * @return void
254
	 * @since 1.23
255
	 */
256
	public function clearPreparedEdit() {
257
		$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...
258
	}
259
260
	/**
261
	 * Return the list of revision fields that should be selected to create
262
	 * a new page.
263
	 *
264
	 * @return array
265
	 */
266
	public static function selectFields() {
267
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
268
269
		$fields = [
270
			'page_id',
271
			'page_namespace',
272
			'page_title',
273
			'page_restrictions',
274
			'page_is_redirect',
275
			'page_is_new',
276
			'page_random',
277
			'page_touched',
278
			'page_links_updated',
279
			'page_latest',
280
			'page_len',
281
		];
282
283
		if ( $wgContentHandlerUseDB ) {
284
			$fields[] = 'page_content_model';
285
		}
286
287
		if ( $wgPageLanguageUseDB ) {
288
			$fields[] = 'page_lang';
289
		}
290
291
		return $fields;
292
	}
293
294
	/**
295
	 * Fetch a page record with the given conditions
296
	 * @param IDatabase $dbr
297
	 * @param array $conditions
298
	 * @param array $options
299
	 * @return object|bool Database result resource, or false on failure
300
	 */
301
	protected function pageData( $dbr, $conditions, $options = [] ) {
302
		$fields = self::selectFields();
303
304
		Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
305
306
		$row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
307
308
		Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
309
310
		return $row;
311
	}
312
313
	/**
314
	 * Fetch a page record matching the Title object's namespace and title
315
	 * using a sanitized title string
316
	 *
317
	 * @param IDatabase $dbr
318
	 * @param Title $title
319
	 * @param array $options
320
	 * @return object|bool Database result resource, or false on failure
321
	 */
322
	public function pageDataFromTitle( $dbr, $title, $options = [] ) {
323
		return $this->pageData( $dbr, [
324
			'page_namespace' => $title->getNamespace(),
325
			'page_title' => $title->getDBkey() ], $options );
326
	}
327
328
	/**
329
	 * Fetch a page record matching the requested ID
330
	 *
331
	 * @param IDatabase $dbr
332
	 * @param int $id
333
	 * @param array $options
334
	 * @return object|bool Database result resource, or false on failure
335
	 */
336
	public function pageDataFromId( $dbr, $id, $options = [] ) {
337
		return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
338
	}
339
340
	/**
341
	 * Load the object from a given source by title
342
	 *
343
	 * @param object|string|int $from One of the following:
344
	 *   - A DB query result object.
345
	 *   - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB.
346
	 *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
347
	 *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
348
	 *     using SELECT FOR UPDATE.
349
	 *
350
	 * @return void
351
	 */
352
	public function loadPageData( $from = 'fromdb' ) {
353
		$from = self::convertSelectType( $from );
354
		if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
355
			// We already have the data from the correct location, no need to load it twice.
356
			return;
357
		}
358
359
		if ( is_int( $from ) ) {
360
			list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
361
			$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
362
363
			if ( !$data
364
				&& $index == DB_SLAVE
365
				&& wfGetLB()->getServerCount() > 1
366
				&& wfGetLB()->hasOrMadeRecentMasterChanges()
367
			) {
368
				$from = self::READ_LATEST;
369
				list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
370
				$data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
371
			}
372
		} else {
373
			// No idea from where the caller got this data, assume slave database.
374
			$data = $from;
375
			$from = self::READ_NORMAL;
376
		}
377
378
		$this->loadFromRow( $data, $from );
0 ignored issues
show
Bug introduced by
It seems like $data defined by $from on line 374 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...
379
	}
380
381
	/**
382
	 * Load the object from a database row
383
	 *
384
	 * @since 1.20
385
	 * @param object|bool $data DB row containing fields returned by selectFields() or false
386
	 * @param string|int $from One of the following:
387
	 *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
388
	 *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
389
	 *        - "forupdate"  or WikiPage::READ_LOCKING if the data comes from
390
	 *          the master DB using SELECT FOR UPDATE
391
	 */
392
	public function loadFromRow( $data, $from ) {
393
		$lc = LinkCache::singleton();
394
		$lc->clearLink( $this->mTitle );
395
396
		if ( $data ) {
397
			$lc->addGoodLinkObjFromRow( $this->mTitle, $data );
398
399
			$this->mTitle->loadFromRow( $data );
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 392 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...
400
401
			// Old-fashioned restrictions
402
			$this->mTitle->loadRestrictions( $data->page_restrictions );
403
404
			$this->mId = intval( $data->page_id );
405
			$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...
406
			$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...
407
			$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...
408
			$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...
409
			// Bug 37225: $latest may no longer match the cached latest Revision object.
410
			// Double-check the ID of any cached latest Revision object for consistency.
411
			if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
412
				$this->mLastRevision = null;
413
				$this->mTimestamp = '';
414
			}
415
		} else {
416
			$lc->addBadLinkObj( $this->mTitle );
417
418
			$this->mTitle->loadFromRow( false );
419
420
			$this->clearCacheFields();
421
422
			$this->mId = 0;
423
		}
424
425
		$this->mDataLoaded = true;
426
		$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...
427
	}
428
429
	/**
430
	 * @return int Page ID
431
	 */
432
	public function getId() {
433
		if ( !$this->mDataLoaded ) {
434
			$this->loadPageData();
435
		}
436
		return $this->mId;
437
	}
438
439
	/**
440
	 * @return bool Whether or not the page exists in the database
441
	 */
442
	public function exists() {
443
		if ( !$this->mDataLoaded ) {
444
			$this->loadPageData();
445
		}
446
		return $this->mId > 0;
447
	}
448
449
	/**
450
	 * Check if this page is something we're going to be showing
451
	 * some sort of sensible content for. If we return false, page
452
	 * views (plain action=view) will return an HTTP 404 response,
453
	 * so spiders and robots can know they're following a bad link.
454
	 *
455
	 * @return bool
456
	 */
457
	public function hasViewableContent() {
458
		return $this->exists() || $this->mTitle->isAlwaysKnown();
459
	}
460
461
	/**
462
	 * Tests if the article content represents a redirect
463
	 *
464
	 * @return bool
465
	 */
466
	public function isRedirect() {
467
		if ( !$this->mDataLoaded ) {
468
			$this->loadPageData();
469
		}
470
471
		return (bool)$this->mIsRedirect;
472
	}
473
474
	/**
475
	 * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
476
	 *
477
	 * Will use the revisions actual content model if the page exists,
478
	 * and the page's default if the page doesn't exist yet.
479
	 *
480
	 * @return string
481
	 *
482
	 * @since 1.21
483
	 */
484
	public function getContentModel() {
485
		if ( $this->exists() ) {
486
			// look at the revision's actual content model
487
			$rev = $this->getRevision();
488
489
			if ( $rev !== null ) {
490
				return $rev->getContentModel();
491
			} else {
492
				$title = $this->mTitle->getPrefixedDBkey();
493
				wfWarn( "Page $title exists but has no (visible) revisions!" );
494
			}
495
		}
496
497
		// use the default model for this page
498
		return $this->mTitle->getContentModel();
499
	}
500
501
	/**
502
	 * Loads page_touched and returns a value indicating if it should be used
503
	 * @return bool True if not a redirect
504
	 */
505
	public function checkTouched() {
506
		if ( !$this->mDataLoaded ) {
507
			$this->loadPageData();
508
		}
509
		return !$this->mIsRedirect;
510
	}
511
512
	/**
513
	 * Get the page_touched field
514
	 * @return string Containing GMT timestamp
515
	 */
516
	public function getTouched() {
517
		if ( !$this->mDataLoaded ) {
518
			$this->loadPageData();
519
		}
520
		return $this->mTouched;
521
	}
522
523
	/**
524
	 * Get the page_links_updated field
525
	 * @return string|null Containing GMT timestamp
526
	 */
527
	public function getLinksTimestamp() {
528
		if ( !$this->mDataLoaded ) {
529
			$this->loadPageData();
530
		}
531
		return $this->mLinksUpdated;
532
	}
533
534
	/**
535
	 * Get the page_latest field
536
	 * @return int The rev_id of current revision
537
	 */
538
	public function getLatest() {
539
		if ( !$this->mDataLoaded ) {
540
			$this->loadPageData();
541
		}
542
		return (int)$this->mLatest;
543
	}
544
545
	/**
546
	 * Get the Revision object of the oldest revision
547
	 * @return Revision|null
548
	 */
549
	public function getOldestRevision() {
550
551
		// Try using the slave database first, then try the master
552
		$continue = 2;
553
		$db = wfGetDB( DB_SLAVE );
554
		$revSelectFields = Revision::selectFields();
555
556
		$row = null;
557
		while ( $continue ) {
558
			$row = $db->selectRow(
559
				[ 'page', 'revision' ],
560
				$revSelectFields,
561
				[
562
					'page_namespace' => $this->mTitle->getNamespace(),
563
					'page_title' => $this->mTitle->getDBkey(),
564
					'rev_page = page_id'
565
				],
566
				__METHOD__,
567
				[
568
					'ORDER BY' => 'rev_timestamp ASC'
569
				]
570
			);
571
572
			if ( $row ) {
573
				$continue = 0;
574
			} else {
575
				$db = wfGetDB( DB_MASTER );
576
				$continue--;
577
			}
578
		}
579
580
		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...
581
	}
582
583
	/**
584
	 * Loads everything except the text
585
	 * This isn't necessary for all uses, so it's only done if needed.
586
	 */
587
	protected function loadLastEdit() {
588
		if ( $this->mLastRevision !== null ) {
589
			return; // already loaded
590
		}
591
592
		$latest = $this->getLatest();
593
		if ( !$latest ) {
594
			return; // page doesn't exist or is missing page_latest info
595
		}
596
597
		if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
598
			// Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
599
			// includes the latest changes committed. This is true even within REPEATABLE-READ
600
			// transactions, where S1 normally only sees changes committed before the first S1
601
			// SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
602
			// may not find it since a page row UPDATE and revision row INSERT by S2 may have
603
			// happened after the first S1 SELECT.
604
			// http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
605
			$flags = Revision::READ_LOCKING;
606
		} elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
607
			// Bug T93976: if page_latest was loaded from the master, fetch the
608
			// revision from there as well, as it may not exist yet on a slave DB.
609
			// Also, this keeps the queries in the same REPEATABLE-READ snapshot.
610
			$flags = Revision::READ_LATEST;
611
		} else {
612
			$flags = 0;
613
		}
614
		$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
615
		if ( $revision ) { // sanity
616
			$this->setLastEdit( $revision );
617
		}
618
	}
619
620
	/**
621
	 * Set the latest revision
622
	 * @param Revision $revision
623
	 */
624
	protected function setLastEdit( Revision $revision ) {
625
		$this->mLastRevision = $revision;
626
		$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...
627
	}
628
629
	/**
630
	 * Get the latest revision
631
	 * @return Revision|null
632
	 */
633
	public function getRevision() {
634
		$this->loadLastEdit();
635
		if ( $this->mLastRevision ) {
636
			return $this->mLastRevision;
637
		}
638
		return null;
639
	}
640
641
	/**
642
	 * Get the content of the current revision. No side-effects...
643
	 *
644
	 * @param int $audience One of:
645
	 *   Revision::FOR_PUBLIC       to be displayed to all users
646
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
647
	 *   Revision::RAW              get the text regardless of permissions
648
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
649
	 *   to the $audience parameter
650
	 * @return Content|null The content of the current revision
651
	 *
652
	 * @since 1.21
653
	 */
654 View Code Duplication
	public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
655
		$this->loadLastEdit();
656
		if ( $this->mLastRevision ) {
657
			return $this->mLastRevision->getContent( $audience, $user );
658
		}
659
		return null;
660
	}
661
662
	/**
663
	 * Get the text of the current revision. No side-effects...
664
	 *
665
	 * @param int $audience One of:
666
	 *   Revision::FOR_PUBLIC       to be displayed to all users
667
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
668
	 *   Revision::RAW              get the text regardless of permissions
669
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
670
	 *   to the $audience parameter
671
	 * @return string|bool The text of the current revision
672
	 * @deprecated since 1.21, getContent() should be used instead.
673
	 */
674
	public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
675
		ContentHandler::deprecated( __METHOD__, '1.21' );
676
677
		$this->loadLastEdit();
678
		if ( $this->mLastRevision ) {
679
			return $this->mLastRevision->getText( $audience, $user );
0 ignored issues
show
Deprecated Code introduced by
The method Revision::getText() has been deprecated with message: since 1.21, use getContent() instead

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

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

Loading history...
680
		}
681
		return false;
682
	}
683
684
	/**
685
	 * @return string MW timestamp of last article revision
686
	 */
687
	public function getTimestamp() {
688
		// Check if the field has been filled by WikiPage::setTimestamp()
689
		if ( !$this->mTimestamp ) {
690
			$this->loadLastEdit();
691
		}
692
693
		return wfTimestamp( TS_MW, $this->mTimestamp );
694
	}
695
696
	/**
697
	 * Set the page timestamp (use only to avoid DB queries)
698
	 * @param string $ts MW timestamp of last article revision
699
	 * @return void
700
	 */
701
	public function setTimestamp( $ts ) {
702
		$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...
703
	}
704
705
	/**
706
	 * @param int $audience One of:
707
	 *   Revision::FOR_PUBLIC       to be displayed to all users
708
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
709
	 *   Revision::RAW              get the text regardless of permissions
710
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
711
	 *   to the $audience parameter
712
	 * @return int User ID for the user that made the last article revision
713
	 */
714 View Code Duplication
	public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
715
		$this->loadLastEdit();
716
		if ( $this->mLastRevision ) {
717
			return $this->mLastRevision->getUser( $audience, $user );
718
		} else {
719
			return -1;
720
		}
721
	}
722
723
	/**
724
	 * Get the User object of the user who created the page
725
	 * @param int $audience One of:
726
	 *   Revision::FOR_PUBLIC       to be displayed to all users
727
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
728
	 *   Revision::RAW              get the text regardless of permissions
729
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
730
	 *   to the $audience parameter
731
	 * @return User|null
732
	 */
733
	public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
734
		$revision = $this->getOldestRevision();
735
		if ( $revision ) {
736
			$userName = $revision->getUserText( $audience, $user );
737
			return User::newFromName( $userName, false );
738
		} else {
739
			return null;
740
		}
741
	}
742
743
	/**
744
	 * @param int $audience One of:
745
	 *   Revision::FOR_PUBLIC       to be displayed to all users
746
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
747
	 *   Revision::RAW              get the text regardless of permissions
748
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
749
	 *   to the $audience parameter
750
	 * @return string Username of the user that made the last article revision
751
	 */
752 View Code Duplication
	public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
753
		$this->loadLastEdit();
754
		if ( $this->mLastRevision ) {
755
			return $this->mLastRevision->getUserText( $audience, $user );
756
		} else {
757
			return '';
758
		}
759
	}
760
761
	/**
762
	 * @param int $audience One of:
763
	 *   Revision::FOR_PUBLIC       to be displayed to all users
764
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
765
	 *   Revision::RAW              get the text regardless of permissions
766
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
767
	 *   to the $audience parameter
768
	 * @return string Comment stored for the last article revision
769
	 */
770 View Code Duplication
	public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
771
		$this->loadLastEdit();
772
		if ( $this->mLastRevision ) {
773
			return $this->mLastRevision->getComment( $audience, $user );
774
		} else {
775
			return '';
776
		}
777
	}
778
779
	/**
780
	 * Returns true if last revision was marked as "minor edit"
781
	 *
782
	 * @return bool Minor edit indicator for the last article revision.
783
	 */
784
	public function getMinorEdit() {
785
		$this->loadLastEdit();
786
		if ( $this->mLastRevision ) {
787
			return $this->mLastRevision->isMinor();
788
		} else {
789
			return false;
790
		}
791
	}
792
793
	/**
794
	 * Determine whether a page would be suitable for being counted as an
795
	 * article in the site_stats table based on the title & its content
796
	 *
797
	 * @param object|bool $editInfo (false): object returned by prepareTextForEdit(),
798
	 *   if false, the current database state will be used
799
	 * @return bool
800
	 */
801
	public function isCountable( $editInfo = false ) {
802
		global $wgArticleCountMethod;
803
804
		if ( !$this->mTitle->isContentPage() ) {
805
			return false;
806
		}
807
808
		if ( $editInfo ) {
809
			$content = $editInfo->pstContent;
810
		} else {
811
			$content = $this->getContent();
812
		}
813
814
		if ( !$content || $content->isRedirect() ) {
815
			return false;
816
		}
817
818
		$hasLinks = null;
819
820
		if ( $wgArticleCountMethod === 'link' ) {
821
			// nasty special case to avoid re-parsing to detect links
822
823
			if ( $editInfo ) {
824
				// ParserOutput::getLinks() is a 2D array of page links, so
825
				// to be really correct we would need to recurse in the array
826
				// but the main array should only have items in it if there are
827
				// links.
828
				$hasLinks = (bool)count( $editInfo->output->getLinks() );
829
			} else {
830
				$hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
831
					[ 'pl_from' => $this->getId() ], __METHOD__ );
832
			}
833
		}
834
835
		return $content->isCountable( $hasLinks );
836
	}
837
838
	/**
839
	 * If this page is a redirect, get its target
840
	 *
841
	 * The target will be fetched from the redirect table if possible.
842
	 * If this page doesn't have an entry there, call insertRedirect()
843
	 * @return Title|null Title object, or null if this page is not a redirect
844
	 */
845
	public function getRedirectTarget() {
846
		if ( !$this->mTitle->isRedirect() ) {
847
			return null;
848
		}
849
850
		if ( $this->mRedirectTarget !== null ) {
851
			return $this->mRedirectTarget;
852
		}
853
854
		// Query the redirect table
855
		$dbr = wfGetDB( DB_SLAVE );
856
		$row = $dbr->selectRow( 'redirect',
857
			[ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
858
			[ 'rd_from' => $this->getId() ],
859
			__METHOD__
860
		);
861
862
		// rd_fragment and rd_interwiki were added later, populate them if empty
863
		if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
864
			$this->mRedirectTarget = Title::makeTitle(
865
				$row->rd_namespace, $row->rd_title,
866
				$row->rd_fragment, $row->rd_interwiki
867
			);
868
			return $this->mRedirectTarget;
869
		}
870
871
		// This page doesn't have an entry in the redirect table
872
		$this->mRedirectTarget = $this->insertRedirect();
873
		return $this->mRedirectTarget;
874
	}
875
876
	/**
877
	 * Insert an entry for this page into the redirect table if the content is a redirect
878
	 *
879
	 * The database update will be deferred via DeferredUpdates
880
	 *
881
	 * Don't call this function directly unless you know what you're doing.
882
	 * @return Title|null Title object or null if not a redirect
883
	 */
884
	public function insertRedirect() {
885
		$content = $this->getContent();
886
		$retval = $content ? $content->getUltimateRedirectTarget() : null;
887
		if ( !$retval ) {
888
			return null;
889
		}
890
891
		// Update the DB post-send if the page has not cached since now
892
		$that = $this;
893
		$latest = $this->getLatest();
894
		DeferredUpdates::addCallableUpdate( function() use ( $that, $retval, $latest ) {
895
			$that->insertRedirectEntry( $retval, $latest );
896
		} );
897
898
		return $retval;
899
	}
900
901
	/**
902
	 * Insert or update the redirect table entry for this page to indicate it redirects to $rt
903
	 * @param Title $rt Redirect target
904
	 * @param int|null $oldLatest Prior page_latest for check and set
905
	 */
906
	public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
907
		$dbw = wfGetDB( DB_MASTER );
908
		$dbw->startAtomic( __METHOD__ );
909
910
		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...
911
			$dbw->replace( 'redirect',
912
				[ 'rd_from' ],
913
				[
914
					'rd_from' => $this->getId(),
915
					'rd_namespace' => $rt->getNamespace(),
916
					'rd_title' => $rt->getDBkey(),
917
					'rd_fragment' => $rt->getFragment(),
918
					'rd_interwiki' => $rt->getInterwiki(),
919
				],
920
				__METHOD__
921
			);
922
		}
923
924
		$dbw->endAtomic( __METHOD__ );
925
	}
926
927
	/**
928
	 * Get the Title object or URL this page redirects to
929
	 *
930
	 * @return bool|Title|string False, Title of in-wiki target, or string with URL
931
	 */
932
	public function followRedirect() {
933
		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...
934
	}
935
936
	/**
937
	 * Get the Title object or URL to use for a redirect. We use Title
938
	 * objects for same-wiki, non-special redirects and URLs for everything
939
	 * else.
940
	 * @param Title $rt Redirect target
941
	 * @return bool|Title|string False, Title object of local target, or string with URL
942
	 */
943
	public function getRedirectURL( $rt ) {
944
		if ( !$rt ) {
945
			return false;
946
		}
947
948
		if ( $rt->isExternal() ) {
949
			if ( $rt->isLocal() ) {
950
				// Offsite wikis need an HTTP redirect.
951
				// This can be hard to reverse and may produce loops,
952
				// so they may be disabled in the site configuration.
953
				$source = $this->mTitle->getFullURL( 'redirect=no' );
954
				return $rt->getFullURL( [ 'rdfrom' => $source ] );
955
			} else {
956
				// External pages without "local" bit set are not valid
957
				// redirect targets
958
				return false;
959
			}
960
		}
961
962
		if ( $rt->isSpecialPage() ) {
963
			// Gotta handle redirects to special pages differently:
964
			// Fill the HTTP response "Location" header and ignore the rest of the page we're on.
965
			// Some pages are not valid targets.
966
			if ( $rt->isValidRedirectTarget() ) {
967
				return $rt->getFullURL();
968
			} else {
969
				return false;
970
			}
971
		}
972
973
		return $rt;
974
	}
975
976
	/**
977
	 * Get a list of users who have edited this article, not including the user who made
978
	 * the most recent revision, which you can get from $article->getUser() if you want it
979
	 * @return UserArrayFromResult
980
	 */
981
	public function getContributors() {
982
		// @todo FIXME: This is expensive; cache this info somewhere.
983
984
		$dbr = wfGetDB( DB_SLAVE );
985
986
		if ( $dbr->implicitGroupby() ) {
987
			$realNameField = 'user_real_name';
988
		} else {
989
			$realNameField = 'MIN(user_real_name) AS user_real_name';
990
		}
991
992
		$tables = [ 'revision', 'user' ];
993
994
		$fields = [
995
			'user_id' => 'rev_user',
996
			'user_name' => 'rev_user_text',
997
			$realNameField,
998
			'timestamp' => 'MAX(rev_timestamp)',
999
		];
1000
1001
		$conds = [ 'rev_page' => $this->getId() ];
1002
1003
		// The user who made the top revision gets credited as "this page was last edited by
1004
		// John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1005
		$user = $this->getUser();
1006
		if ( $user ) {
1007
			$conds[] = "rev_user != $user";
1008
		} else {
1009
			$conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1010
		}
1011
1012
		// Username hidden?
1013
		$conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1014
1015
		$jconds = [
1016
			'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
1017
		];
1018
1019
		$options = [
1020
			'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
1021
			'ORDER BY' => 'timestamp DESC',
1022
		];
1023
1024
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1025
		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 1024 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...
1026
	}
1027
1028
	/**
1029
	 * Should the parser cache be used?
1030
	 *
1031
	 * @param ParserOptions $parserOptions ParserOptions to check
1032
	 * @param int $oldId
1033
	 * @return bool
1034
	 */
1035
	public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
1036
		return $parserOptions->getStubThreshold() == 0
1037
			&& $this->exists()
1038
			&& ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
1039
			&& $this->getContentHandler()->isParserCacheSupported();
1040
	}
1041
1042
	/**
1043
	 * Get a ParserOutput for the given ParserOptions and revision ID.
1044
	 *
1045
	 * The parser cache will be used if possible. Cache misses that result
1046
	 * in parser runs are debounced with PoolCounter.
1047
	 *
1048
	 * @since 1.19
1049
	 * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
1050
	 * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
1051
	 *   get the current revision (default value)
1052
	 *
1053
	 * @return ParserOutput|bool ParserOutput or false if the revision was not found
1054
	 */
1055
	public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
1056
1057
		$useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
1058
		wfDebug( __METHOD__ .
1059
			': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1060
		if ( $parserOptions->getStubThreshold() ) {
1061
			wfIncrStats( 'pcache.miss.stub' );
1062
		}
1063
1064
		if ( $useParserCache ) {
1065
			$parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1066
			if ( $parserOutput !== false ) {
1067
				return $parserOutput;
1068
			}
1069
		}
1070
1071
		if ( $oldid === null || $oldid === 0 ) {
1072
			$oldid = $this->getLatest();
1073
		}
1074
1075
		$pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1076
		$pool->execute();
1077
1078
		return $pool->getParserOutput();
1079
	}
1080
1081
	/**
1082
	 * Do standard deferred updates after page view (existing or missing page)
1083
	 * @param User $user The relevant user
1084
	 * @param int $oldid Revision id being viewed; if not given or 0, latest revision is assumed
1085
	 */
1086
	public function doViewUpdates( User $user, $oldid = 0 ) {
1087
		if ( wfReadOnly() ) {
1088
			return;
1089
		}
1090
1091
		Hooks::run( 'PageViewUpdates', [ $this, $user ] );
1092
		// Update newtalk / watchlist notification status
1093
		try {
1094
			$user->clearNotification( $this->mTitle, $oldid );
1095
		} catch ( DBError $e ) {
1096
			// Avoid outage if the master is not reachable
1097
			MWExceptionHandler::logException( $e );
1098
		}
1099
	}
1100
1101
	/**
1102
	 * Perform the actions of a page purging
1103
	 * @return bool
1104
	 */
1105
	public function doPurge() {
1106
		if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
1107
			return false;
1108
		}
1109
1110
		$title = $this->mTitle;
1111
		wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
1112
			// Invalidate the cache in auto-commit mode
1113
			$title->invalidateCache();
1114
		} );
1115
1116
		// Send purge after above page_touched update was committed
1117
		DeferredUpdates::addUpdate(
1118
			new CdnCacheUpdate( $title->getCdnUrls() ),
1119
			DeferredUpdates::PRESEND
1120
		);
1121
1122
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1123
			// @todo move this logic to MessageCache
1124
			if ( $this->exists() ) {
1125
				// NOTE: use transclusion text for messages.
1126
				//       This is consistent with  MessageCache::getMsgFromNamespace()
1127
1128
				$content = $this->getContent();
1129
				$text = $content === null ? null : $content->getWikitextForTransclusion();
1130
1131
				if ( $text === null ) {
1132
					$text = false;
1133
				}
1134
			} else {
1135
				$text = false;
1136
			}
1137
1138
			MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1139
		}
1140
1141
		return true;
1142
	}
1143
1144
	/**
1145
	 * Insert a new empty page record for this article.
1146
	 * This *must* be followed up by creating a revision
1147
	 * and running $this->updateRevisionOn( ... );
1148
	 * or else the record will be left in a funky state.
1149
	 * Best if all done inside a transaction.
1150
	 *
1151
	 * @param IDatabase $dbw
1152
	 * @param int|null $pageId Custom page ID that will be used for the insert statement
1153
	 *
1154
	 * @return bool|int The newly created page_id key; false if the title already existed
1155
	 */
1156
	public function insertOn( $dbw, $pageId = null ) {
1157
		$pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
1158
		$dbw->insert(
1159
			'page',
1160
			[
1161
				'page_id'           => $pageIdForInsert,
1162
				'page_namespace'    => $this->mTitle->getNamespace(),
1163
				'page_title'        => $this->mTitle->getDBkey(),
1164
				'page_restrictions' => '',
1165
				'page_is_redirect'  => 0, // Will set this shortly...
1166
				'page_is_new'       => 1,
1167
				'page_random'       => wfRandom(),
1168
				'page_touched'      => $dbw->timestamp(),
1169
				'page_latest'       => 0, // Fill this in shortly...
1170
				'page_len'          => 0, // Fill this in shortly...
1171
			],
1172
			__METHOD__,
1173
			'IGNORE'
1174
		);
1175
1176
		if ( $dbw->affectedRows() > 0 ) {
1177
			$newid = $pageId ?: $dbw->insertId();
1178
			$this->mId = $newid;
1179
			$this->mTitle->resetArticleID( $newid );
1180
1181
			return $newid;
1182
		} else {
1183
			return false; // nothing changed
1184
		}
1185
	}
1186
1187
	/**
1188
	 * Update the page record to point to a newly saved revision.
1189
	 *
1190
	 * @param IDatabase $dbw
1191
	 * @param Revision $revision For ID number, and text used to set
1192
	 *   length and redirect status fields
1193
	 * @param int $lastRevision If given, will not overwrite the page field
1194
	 *   when different from the currently set value.
1195
	 *   Giving 0 indicates the new page flag should be set on.
1196
	 * @param bool $lastRevIsRedirect If given, will optimize adding and
1197
	 *   removing rows in redirect table.
1198
	 * @return bool Success; false if the page row was missing or page_latest changed
1199
	 */
1200
	public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1201
		$lastRevIsRedirect = null
1202
	) {
1203
		global $wgContentHandlerUseDB;
1204
1205
		// Assertion to try to catch T92046
1206
		if ( (int)$revision->getId() === 0 ) {
1207
			throw new InvalidArgumentException(
1208
				__METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
1209
			);
1210
		}
1211
1212
		$content = $revision->getContent();
1213
		$len = $content ? $content->getSize() : 0;
1214
		$rt = $content ? $content->getUltimateRedirectTarget() : null;
1215
1216
		$conditions = [ 'page_id' => $this->getId() ];
1217
1218
		if ( !is_null( $lastRevision ) ) {
1219
			// An extra check against threads stepping on each other
1220
			$conditions['page_latest'] = $lastRevision;
1221
		}
1222
1223
		$row = [ /* SET */
1224
			'page_latest'      => $revision->getId(),
1225
			'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
1226
			'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
1227
			'page_is_redirect' => $rt !== null ? 1 : 0,
1228
			'page_len'         => $len,
1229
		];
1230
1231
		if ( $wgContentHandlerUseDB ) {
1232
			$row['page_content_model'] = $revision->getContentModel();
1233
		}
1234
1235
		$dbw->update( 'page',
1236
			$row,
1237
			$conditions,
1238
			__METHOD__ );
1239
1240
		$result = $dbw->affectedRows() > 0;
1241
		if ( $result ) {
1242
			$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 1214 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...
1243
			$this->setLastEdit( $revision );
1244
			$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...
1245
			$this->mIsRedirect = (bool)$rt;
1246
			// Update the LinkCache.
1247
			LinkCache::singleton()->addGoodLinkObj(
1248
				$this->getId(),
1249
				$this->mTitle,
1250
				$len,
1251
				$this->mIsRedirect,
1252
				$this->mLatest,
1253
				$revision->getContentModel()
1254
			);
1255
		}
1256
1257
		return $result;
1258
	}
1259
1260
	/**
1261
	 * Add row to the redirect table if this is a redirect, remove otherwise.
1262
	 *
1263
	 * @param IDatabase $dbw
1264
	 * @param Title $redirectTitle Title object pointing to the redirect target,
1265
	 *   or NULL if this is not a redirect
1266
	 * @param null|bool $lastRevIsRedirect If given, will optimize adding and
1267
	 *   removing rows in redirect table.
1268
	 * @return bool True on success, false on failure
1269
	 * @private
1270
	 */
1271
	public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1272
		// Always update redirects (target link might have changed)
1273
		// Update/Insert if we don't know if the last revision was a redirect or not
1274
		// Delete if changing from redirect to non-redirect
1275
		$isRedirect = !is_null( $redirectTitle );
1276
1277
		if ( !$isRedirect && $lastRevIsRedirect === false ) {
1278
			return true;
1279
		}
1280
1281
		if ( $isRedirect ) {
1282
			$this->insertRedirectEntry( $redirectTitle );
1283
		} else {
1284
			// This is not a redirect, remove row from redirect table
1285
			$where = [ 'rd_from' => $this->getId() ];
1286
			$dbw->delete( 'redirect', $where, __METHOD__ );
1287
		}
1288
1289
		if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1290
			RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1291
		}
1292
1293
		return ( $dbw->affectedRows() != 0 );
1294
	}
1295
1296
	/**
1297
	 * If the given revision is newer than the currently set page_latest,
1298
	 * update the page record. Otherwise, do nothing.
1299
	 *
1300
	 * @deprecated since 1.24, use updateRevisionOn instead
1301
	 *
1302
	 * @param IDatabase $dbw
1303
	 * @param Revision $revision
1304
	 * @return bool
1305
	 */
1306
	public function updateIfNewerOn( $dbw, $revision ) {
1307
1308
		$row = $dbw->selectRow(
1309
			[ 'revision', 'page' ],
1310
			[ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
1311
			[
1312
				'page_id' => $this->getId(),
1313
				'page_latest=rev_id' ],
1314
			__METHOD__ );
1315
1316
		if ( $row ) {
1317
			if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1318
				return false;
1319
			}
1320
			$prev = $row->rev_id;
1321
			$lastRevIsRedirect = (bool)$row->page_is_redirect;
1322
		} else {
1323
			// No or missing previous revision; mark the page as new
1324
			$prev = 0;
1325
			$lastRevIsRedirect = null;
1326
		}
1327
1328
		$ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1329
1330
		return $ret;
1331
	}
1332
1333
	/**
1334
	 * Get the content that needs to be saved in order to undo all revisions
1335
	 * between $undo and $undoafter. Revisions must belong to the same page,
1336
	 * must exist and must not be deleted
1337
	 * @param Revision $undo
1338
	 * @param Revision $undoafter Must be an earlier revision than $undo
1339
	 * @return Content|bool Content on success, false on failure
1340
	 * @since 1.21
1341
	 * Before we had the Content object, this was done in getUndoText
1342
	 */
1343
	public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1344
		$handler = $undo->getContentHandler();
1345
		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 1343 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...
1346
	}
1347
1348
	/**
1349
	 * Returns true if this page's content model supports sections.
1350
	 *
1351
	 * @return bool
1352
	 *
1353
	 * @todo The skin should check this and not offer section functionality if
1354
	 *   sections are not supported.
1355
	 * @todo The EditPage should check this and not offer section functionality
1356
	 *   if sections are not supported.
1357
	 */
1358
	public function supportsSections() {
1359
		return $this->getContentHandler()->supportsSections();
1360
	}
1361
1362
	/**
1363
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1364
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1365
	 * or 'new' for a new section.
1366
	 * @param Content $sectionContent New content of the section.
1367
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1368
	 * @param string $edittime Revision timestamp or null to use the current revision.
1369
	 *
1370
	 * @throws MWException
1371
	 * @return Content|null New complete article content, or null if error.
1372
	 *
1373
	 * @since 1.21
1374
	 * @deprecated since 1.24, use replaceSectionAtRev instead
1375
	 */
1376
	public function replaceSectionContent(
1377
		$sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
1378
	) {
1379
1380
		$baseRevId = null;
1381
		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...
1382
			$dbr = wfGetDB( DB_SLAVE );
1383
			$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
1384
			// Try the master if this thread may have just added it.
1385
			// This could be abstracted into a Revision method, but we don't want
1386
			// to encourage loading of revisions by timestamp.
1387
			if ( !$rev
1388
				&& wfGetLB()->getServerCount() > 1
1389
				&& wfGetLB()->hasOrMadeRecentMasterChanges()
1390
			) {
1391
				$dbw = wfGetDB( DB_MASTER );
1392
				$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1393
			}
1394
			if ( $rev ) {
1395
				$baseRevId = $rev->getId();
1396
			}
1397
		}
1398
1399
		return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1400
	}
1401
1402
	/**
1403
	 * @param string|number|null|bool $sectionId Section identifier as a number or string
1404
	 * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1405
	 * or 'new' for a new section.
1406
	 * @param Content $sectionContent New content of the section.
1407
	 * @param string $sectionTitle New section's subject, only if $section is "new".
1408
	 * @param int|null $baseRevId
1409
	 *
1410
	 * @throws MWException
1411
	 * @return Content|null New complete article content, or null if error.
1412
	 *
1413
	 * @since 1.24
1414
	 */
1415
	public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1416
		$sectionTitle = '', $baseRevId = null
1417
	) {
1418
1419
		if ( strval( $sectionId ) === '' ) {
1420
			// Whole-page edit; let the whole text through
1421
			$newContent = $sectionContent;
1422
		} else {
1423
			if ( !$this->supportsSections() ) {
1424
				throw new MWException( "sections not supported for content model " .
1425
					$this->getContentHandler()->getModelID() );
1426
			}
1427
1428
			// Bug 30711: always use current version when adding a new section
1429
			if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1430
				$oldContent = $this->getContent();
1431
			} else {
1432
				$rev = Revision::newFromId( $baseRevId );
1433
				if ( !$rev ) {
1434
					wfDebug( __METHOD__ . " asked for bogus section (page: " .
1435
						$this->getId() . "; section: $sectionId)\n" );
1436
					return null;
1437
				}
1438
1439
				$oldContent = $rev->getContent();
1440
			}
1441
1442
			if ( !$oldContent ) {
1443
				wfDebug( __METHOD__ . ": no page text\n" );
1444
				return null;
1445
			}
1446
1447
			$newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1448
		}
1449
1450
		return $newContent;
1451
	}
1452
1453
	/**
1454
	 * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
1455
	 * @param int $flags
1456
	 * @return int Updated $flags
1457
	 */
1458
	public function checkFlags( $flags ) {
1459
		if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1460
			if ( $this->exists() ) {
1461
				$flags |= EDIT_UPDATE;
1462
			} else {
1463
				$flags |= EDIT_NEW;
1464
			}
1465
		}
1466
1467
		return $flags;
1468
	}
1469
1470
	/**
1471
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1472
	 * optionally via the deferred update array.
1473
	 *
1474
	 * @param string $text New text
1475
	 * @param string $summary Edit summary
1476
	 * @param int $flags Bitfield:
1477
	 *      EDIT_NEW
1478
	 *          Article is known or assumed to be non-existent, create a new one
1479
	 *      EDIT_UPDATE
1480
	 *          Article is known or assumed to be pre-existing, update it
1481
	 *      EDIT_MINOR
1482
	 *          Mark this edit minor, if the user is allowed to do so
1483
	 *      EDIT_SUPPRESS_RC
1484
	 *          Do not log the change in recentchanges
1485
	 *      EDIT_FORCE_BOT
1486
	 *          Mark the edit a "bot" edit regardless of user rights
1487
	 *      EDIT_AUTOSUMMARY
1488
	 *          Fill in blank summaries with generated text where possible
1489
	 *
1490
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1491
	 * article will be detected. If EDIT_UPDATE is specified and the article
1492
	 * doesn't exist, the function will return an edit-gone-missing error. If
1493
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1494
	 * error will be returned. These two conditions are also possible with
1495
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1496
	 *
1497
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1498
	 *   This is not the parent revision ID, rather the revision ID for older
1499
	 *   content used as the source for a rollback, for example.
1500
	 * @param User $user The user doing the edit
1501
	 *
1502
	 * @throws MWException
1503
	 * @return Status Possible errors:
1504
	 *   edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1505
	 *     set the fatal flag of $status
1506
	 *   edit-gone-missing: In update mode, but the article didn't exist.
1507
	 *   edit-conflict: In update mode, the article changed unexpectedly.
1508
	 *   edit-no-change: Warning that the text was the same as before.
1509
	 *   edit-already-exists: In creation mode, but the article already exists.
1510
	 *
1511
	 * Extensions may define additional errors.
1512
	 *
1513
	 * $return->value will contain an associative array with members as follows:
1514
	 *     new: Boolean indicating if the function attempted to create a new article.
1515
	 *     revision: The revision object for the inserted revision, or null.
1516
	 *
1517
	 * Compatibility note: this function previously returned a boolean value
1518
	 * indicating success/failure
1519
	 *
1520
	 * @deprecated since 1.21: use doEditContent() instead.
1521
	 */
1522
	public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1523
		ContentHandler::deprecated( __METHOD__, '1.21' );
1524
1525
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
1526
1527
		return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1528
	}
1529
1530
	/**
1531
	 * Change an existing article or create a new article. Updates RC and all necessary caches,
1532
	 * optionally via the deferred update array.
1533
	 *
1534
	 * @param Content $content New content
1535
	 * @param string $summary Edit summary
1536
	 * @param int $flags Bitfield:
1537
	 *      EDIT_NEW
1538
	 *          Article is known or assumed to be non-existent, create a new one
1539
	 *      EDIT_UPDATE
1540
	 *          Article is known or assumed to be pre-existing, update it
1541
	 *      EDIT_MINOR
1542
	 *          Mark this edit minor, if the user is allowed to do so
1543
	 *      EDIT_SUPPRESS_RC
1544
	 *          Do not log the change in recentchanges
1545
	 *      EDIT_FORCE_BOT
1546
	 *          Mark the edit a "bot" edit regardless of user rights
1547
	 *      EDIT_AUTOSUMMARY
1548
	 *          Fill in blank summaries with generated text where possible
1549
	 *
1550
	 * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1551
	 * article will be detected. If EDIT_UPDATE is specified and the article
1552
	 * doesn't exist, the function will return an edit-gone-missing error. If
1553
	 * EDIT_NEW is specified and the article does exist, an edit-already-exists
1554
	 * error will be returned. These two conditions are also possible with
1555
	 * auto-detection due to MediaWiki's performance-optimised locking strategy.
1556
	 *
1557
	 * @param bool|int $baseRevId The revision ID this edit was based off, if any.
1558
	 *   This is not the parent revision ID, rather the revision ID for older
1559
	 *   content used as the source for a rollback, for example.
1560
	 * @param User $user The user doing the edit
1561
	 * @param string $serialFormat Format for storing the content in the
1562
	 *   database.
1563
	 * @param array|null $tags Change tags to apply to this edit
1564
	 * Callers are responsible for permission checks
1565
	 * (with ChangeTags::canAddTagsAccompanyingChange)
1566
	 *
1567
	 * @throws MWException
1568
	 * @return Status Possible errors:
1569
	 *     edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1570
	 *       set the fatal flag of $status.
1571
	 *     edit-gone-missing: In update mode, but the article didn't exist.
1572
	 *     edit-conflict: In update mode, the article changed unexpectedly.
1573
	 *     edit-no-change: Warning that the text was the same as before.
1574
	 *     edit-already-exists: In creation mode, but the article already exists.
1575
	 *
1576
	 *  Extensions may define additional errors.
1577
	 *
1578
	 *  $return->value will contain an associative array with members as follows:
1579
	 *     new: Boolean indicating if the function attempted to create a new article.
1580
	 *     revision: The revision object for the inserted revision, or null.
1581
	 *
1582
	 * @since 1.21
1583
	 * @throws MWException
1584
	 */
1585
	public function doEditContent(
1586
		Content $content, $summary, $flags = 0, $baseRevId = false,
1587
		User $user = null, $serialFormat = null, $tags = null
1588
	) {
1589
		global $wgUser, $wgUseAutomaticEditSummaries;
1590
1591
		// Low-level sanity check
1592
		if ( $this->mTitle->getText() === '' ) {
1593
			throw new MWException( 'Something is trying to edit an article with an empty title' );
1594
		}
1595
		// Make sure the given content type is allowed for this page
1596
		if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
1597
			return Status::newFatal( 'content-not-allowed-here',
1598
				ContentHandler::getLocalizedName( $content->getModel() ),
1599
				$this->mTitle->getPrefixedText()
1600
			);
1601
		}
1602
1603
		// Load the data from the master database if needed.
1604
		// The caller may already loaded it from the master or even loaded it using
1605
		// SELECT FOR UPDATE, so do not override that using clear().
1606
		$this->loadPageData( 'fromdbmaster' );
1607
1608
		$user = $user ?: $wgUser;
1609
		$flags = $this->checkFlags( $flags );
1610
1611
		// Trigger pre-save hook (using provided edit summary)
1612
		$hookStatus = Status::newGood( [] );
1613
		$hook_args = [ &$this, &$user, &$content, &$summary,
1614
							$flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
1615
		// Check if the hook rejected the attempted save
1616
		if ( !Hooks::run( 'PageContentSave', $hook_args )
1617
			|| !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
1618
		) {
1619
			if ( $hookStatus->isOK() ) {
1620
				// Hook returned false but didn't call fatal(); use generic message
1621
				$hookStatus->fatal( 'edit-hook-aborted' );
1622
			}
1623
1624
			return $hookStatus;
1625
		}
1626
1627
		$old_revision = $this->getRevision(); // current revision
1628
		$old_content = $this->getContent( Revision::RAW ); // current revision's content
1629
1630
		// Provide autosummaries if one is not provided and autosummaries are enabled
1631
		if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
1632
			$handler = $content->getContentHandler();
1633
			$summary = $handler->getAutosummary( $old_content, $content, $flags );
1634
		}
1635
1636
		// Get the pre-save transform content and final parser output
1637
		$editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat );
1638
		$pstContent = $editInfo->pstContent; // Content object
1639
		$meta = [
1640
			'bot' => ( $flags & EDIT_FORCE_BOT ),
1641
			'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
1642
			'serialized' => $editInfo->pst,
1643
			'serialFormat' => $serialFormat,
1644
			'baseRevId' => $baseRevId,
1645
			'oldRevision' => $old_revision,
1646
			'oldContent' => $old_content,
1647
			'oldId' => $this->getLatest(),
1648
			'oldIsRedirect' => $this->isRedirect(),
1649
			'oldCountable' => $this->isCountable(),
1650
			'tags' => ( $tags !== null ) ? (array)$tags : []
1651
		];
1652
1653
		// Actually create the revision and create/update the page
1654
		if ( $flags & EDIT_UPDATE ) {
1655
			$status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
1656
		} else {
1657
			$status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
1658
		}
1659
1660
		// Promote user to any groups they meet the criteria for
1661
		DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1662
			$user->addAutopromoteOnceGroups( 'onEdit' );
1663
			$user->addAutopromoteOnceGroups( 'onView' ); // b/c
1664
		} );
1665
1666
		return $status;
1667
	}
1668
1669
	/**
1670
	 * @param Content $content Pre-save transform content
1671
	 * @param integer $flags
1672
	 * @param User $user
1673
	 * @param string $summary
1674
	 * @param array $meta
1675
	 * @return Status
1676
	 * @throws DBUnexpectedError
1677
	 * @throws Exception
1678
	 * @throws FatalError
1679
	 * @throws MWException
1680
	 */
1681
	private function doModify(
1682
		Content $content, $flags, User $user, $summary, array $meta
1683
	) {
1684
		global $wgUseRCPatrol;
1685
1686
		// Update article, but only if changed.
1687
		$status = Status::newGood( [ 'new' => false, 'revision' => null ] );
1688
1689
		// Convenience variables
1690
		$now = wfTimestampNow();
1691
		$oldid = $meta['oldId'];
1692
		/** @var $oldContent Content|null */
1693
		$oldContent = $meta['oldContent'];
1694
		$newsize = $content->getSize();
1695
1696
		if ( !$oldid ) {
1697
			// Article gone missing
1698
			$status->fatal( 'edit-gone-missing' );
1699
1700
			return $status;
1701
		} elseif ( !$oldContent ) {
1702
			// Sanity check for bug 37225
1703
			throw new MWException( "Could not find text for current revision {$oldid}." );
1704
		}
1705
1706
		// @TODO: pass content object?!
1707
		$revision = new Revision( [
1708
			'page'       => $this->getId(),
1709
			'title'      => $this->mTitle, // for determining the default content model
1710
			'comment'    => $summary,
1711
			'minor_edit' => $meta['minor'],
1712
			'text'       => $meta['serialized'],
1713
			'len'        => $newsize,
1714
			'parent_id'  => $oldid,
1715
			'user'       => $user->getId(),
1716
			'user_text'  => $user->getName(),
1717
			'timestamp'  => $now,
1718
			'content_model' => $content->getModel(),
1719
			'content_format' => $meta['serialFormat'],
1720
		] );
1721
1722
		$changed = !$content->equals( $oldContent );
1723
1724
		$dbw = wfGetDB( DB_MASTER );
1725
1726
		if ( $changed ) {
1727
			$prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
1728
			$status->merge( $prepStatus );
1729
			if ( !$status->isOK() ) {
1730
				return $status;
1731
			}
1732
1733
			$dbw->startAtomic( __METHOD__ );
1734
			// Get the latest page_latest value while locking it.
1735
			// Do a CAS style check to see if it's the same as when this method
1736
			// started. If it changed then bail out before touching the DB.
1737
			$latestNow = $this->lockAndGetLatest();
1738
			if ( $latestNow != $oldid ) {
1739
				$dbw->endAtomic( __METHOD__ );
1740
				// Page updated or deleted in the mean time
1741
				$status->fatal( 'edit-conflict' );
1742
1743
				return $status;
1744
			}
1745
1746
			// At this point we are now comitted to returning an OK
1747
			// status unless some DB query error or other exception comes up.
1748
			// This way callers don't have to call rollback() if $status is bad
1749
			// unless they actually try to catch exceptions (which is rare).
1750
1751
			// Save the revision text
1752
			$revisionId = $revision->insertOn( $dbw );
1753
			// Update page_latest and friends to reflect the new revision
1754
			if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
1755
				$dbw->rollback( __METHOD__ ); // sanity; this should never happen
1756
				throw new MWException( "Failed to update page row to use new revision." );
1757
			}
1758
1759
			Hooks::run( 'NewRevisionFromEditComplete',
1760
				[ $this, $revision, $meta['baseRevId'], $user ] );
1761
1762
			// Update recentchanges
1763
			if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1764
				// Mark as patrolled if the user can do so
1765
				$patrolled = $wgUseRCPatrol && !count(
1766
						$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1767
				// Add RC row to the DB
1768
				RecentChange::notifyEdit(
1769
					$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1690 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...
1770
					$this->mTitle,
1771
					$revision->isMinor(),
1772
					$user,
1773
					$summary,
1774
					$oldid,
1775
					$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...
1776
					$meta['bot'],
1777
					'',
1778
					$oldContent ? $oldContent->getSize() : 0,
1779
					$newsize,
1780
					$revisionId,
1781
					$patrolled,
1782
					$meta['tags']
1783
				);
1784
			}
1785
1786
			$user->incEditCount();
1787
1788
			$dbw->endAtomic( __METHOD__ );
1789
			$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...
1790
		} else {
1791
			// Bug 32948: revision ID must be set to page {{REVISIONID}} and
1792
			// related variables correctly
1793
			$revision->setId( $this->getLatest() );
1794
		}
1795
1796
		if ( $changed ) {
1797
			// Return the new revision to the caller
1798
			$status->value['revision'] = $revision;
1799
		} else {
1800
			$status->warning( 'edit-no-change' );
1801
			// Update page_touched as updateRevisionOn() was not called.
1802
			// Other cache updates are managed in onArticleEdit() via doEditUpdates().
1803
			$this->mTitle->invalidateCache( $now );
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1690 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...
1804
		}
1805
1806
		// Do secondary updates once the main changes have been committed...
1807
		$that = $this;
1808
		$dbw->onTransactionIdle(
1809
			function () use (
1810
				$dbw, &$that, $revision, &$user, $content, $summary, &$flags,
1811
				$changed, $meta, &$status
1812
			) {
1813
				// Do per-page updates in a transaction
1814
				$dbw->setFlag( DBO_TRX );
1815
				// Update links tables, site stats, etc.
1816
				$that->doEditUpdates(
1817
					$revision,
1818
					$user,
1819
					[
1820
						'changed' => $changed,
1821
						'oldcountable' => $meta['oldCountable'],
1822
						'oldrevision' => $meta['oldRevision']
1823
					]
1824
				);
1825
				// Trigger post-save hook
1826
				$params = [ &$that, &$user, $content, $summary, $flags & EDIT_MINOR,
1827
					null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
1828
				ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1829
				Hooks::run( 'PageContentSaveComplete', $params );
1830
			}
1831
		);
1832
1833
		return $status;
1834
	}
1835
1836
	/**
1837
	 * @param Content $content Pre-save transform content
1838
	 * @param integer $flags
1839
	 * @param User $user
1840
	 * @param string $summary
1841
	 * @param array $meta
1842
	 * @return Status
1843
	 * @throws DBUnexpectedError
1844
	 * @throws Exception
1845
	 * @throws FatalError
1846
	 * @throws MWException
1847
	 */
1848
	private function doCreate(
1849
		Content $content, $flags, User $user, $summary, array $meta
1850
	) {
1851
		global $wgUseRCPatrol, $wgUseNPPatrol;
1852
1853
		$status = Status::newGood( [ 'new' => true, 'revision' => null ] );
1854
1855
		$now = wfTimestampNow();
1856
		$newsize = $content->getSize();
1857
		$prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
1858
		$status->merge( $prepStatus );
1859
		if ( !$status->isOK() ) {
1860
			return $status;
1861
		}
1862
1863
		$dbw = wfGetDB( DB_MASTER );
1864
		$dbw->startAtomic( __METHOD__ );
1865
1866
		// Add the page record unless one already exists for the title
1867
		$newid = $this->insertOn( $dbw );
1868
		if ( $newid === false ) {
1869
			$dbw->endAtomic( __METHOD__ ); // nothing inserted
1870
			$status->fatal( 'edit-already-exists' );
1871
1872
			return $status; // nothing done
1873
		}
1874
1875
		// At this point we are now comitted to returning an OK
1876
		// status unless some DB query error or other exception comes up.
1877
		// This way callers don't have to call rollback() if $status is bad
1878
		// unless they actually try to catch exceptions (which is rare).
1879
1880
		// @TODO: pass content object?!
1881
		$revision = new Revision( [
1882
			'page'       => $newid,
1883
			'title'      => $this->mTitle, // for determining the default content model
1884
			'comment'    => $summary,
1885
			'minor_edit' => $meta['minor'],
1886
			'text'       => $meta['serialized'],
1887
			'len'        => $newsize,
1888
			'user'       => $user->getId(),
1889
			'user_text'  => $user->getName(),
1890
			'timestamp'  => $now,
1891
			'content_model' => $content->getModel(),
1892
			'content_format' => $meta['serialFormat'],
1893
		] );
1894
1895
		// Save the revision text...
1896
		$revisionId = $revision->insertOn( $dbw );
1897
		// Update the page record with revision data
1898
		if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
1899
			$dbw->rollback( __METHOD__ ); // sanity; this should never happen
1900
			throw new MWException( "Failed to update page row to use new revision." );
1901
		}
1902
1903
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
1904
1905
		// Update recentchanges
1906
		if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1907
			// Mark as patrolled if the user can do so
1908
			$patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
1909
				!count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1910
			// Add RC row to the DB
1911
			RecentChange::notifyNew(
1912
				$now,
0 ignored issues
show
Security Bug introduced by
It seems like $now defined by wfTimestampNow() on line 1855 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...
1913
				$this->mTitle,
1914
				$revision->isMinor(),
1915
				$user,
1916
				$summary,
1917
				$meta['bot'],
1918
				'',
1919
				$newsize,
1920
				$revisionId,
1921
				$patrolled,
1922
				$meta['tags']
1923
			);
1924
		}
1925
1926
		$user->incEditCount();
1927
1928
		$dbw->endAtomic( __METHOD__ );
1929
		$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...
1930
1931
		// Return the new revision to the caller
1932
		$status->value['revision'] = $revision;
1933
1934
		// Do secondary updates once the main changes have been committed...
1935
		$that = $this;
1936
		$dbw->onTransactionIdle(
1937
			function () use (
1938
				&$that, $dbw, $revision, &$user, $content, $summary, &$flags, $meta, &$status
1939
			) {
1940
				// Do per-page updates in a transaction
1941
				$dbw->setFlag( DBO_TRX );
1942
				// Update links, etc.
1943
				$that->doEditUpdates( $revision, $user, [ 'created' => true ] );
1944
				// Trigger post-create hook
1945
				$params = [ &$that, &$user, $content, $summary,
1946
					$flags & EDIT_MINOR, null, null, &$flags, $revision ];
1947
				ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
1948
				Hooks::run( 'PageContentInsertComplete', $params );
1949
				// Trigger post-save hook
1950
				$params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
1951
				ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
1952
				Hooks::run( 'PageContentSaveComplete', $params );
1953
1954
			}
1955
		);
1956
1957
		return $status;
1958
	}
1959
1960
	/**
1961
	 * Get parser options suitable for rendering the primary article wikitext
1962
	 *
1963
	 * @see ContentHandler::makeParserOptions
1964
	 *
1965
	 * @param IContextSource|User|string $context One of the following:
1966
	 *        - IContextSource: Use the User and the Language of the provided
1967
	 *          context
1968
	 *        - User: Use the provided User object and $wgLang for the language,
1969
	 *          so use an IContextSource object if possible.
1970
	 *        - 'canonical': Canonical options (anonymous user with default
1971
	 *          preferences and content language).
1972
	 * @return ParserOptions
1973
	 */
1974
	public function makeParserOptions( $context ) {
1975
		$options = $this->getContentHandler()->makeParserOptions( $context );
1976
1977
		if ( $this->getTitle()->isConversionTable() ) {
1978
			// @todo ConversionTable should become a separate content model, so
1979
			// we don't need special cases like this one.
1980
			$options->disableContentConversion();
1981
		}
1982
1983
		return $options;
1984
	}
1985
1986
	/**
1987
	 * Prepare text which is about to be saved.
1988
	 * Returns a stdClass with source, pst and output members
1989
	 *
1990
	 * @param string $text
1991
	 * @param int|null $revid
1992
	 * @param User|null $user
1993
	 * @deprecated since 1.21: use prepareContentForEdit instead.
1994
	 * @return object
1995
	 */
1996
	public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
1997
		ContentHandler::deprecated( __METHOD__, '1.21' );
1998
		$content = ContentHandler::makeContent( $text, $this->getTitle() );
1999
		return $this->prepareContentForEdit( $content, $revid, $user );
2000
	}
2001
2002
	/**
2003
	 * Prepare content which is about to be saved.
2004
	 * Returns a stdClass with source, pst and output members
2005
	 *
2006
	 * @param Content $content
2007
	 * @param Revision|int|null $revision Revision object. For backwards compatibility, a
2008
	 *        revision ID is also accepted, but this is deprecated.
2009
	 * @param User|null $user
2010
	 * @param string|null $serialFormat
2011
	 * @param bool $useCache Check shared prepared edit cache
2012
	 *
2013
	 * @return object
2014
	 *
2015
	 * @since 1.21
2016
	 */
2017
	public function prepareContentForEdit(
2018
		Content $content, $revision = null, User $user = null,
2019
		$serialFormat = null, $useCache = true
2020
	) {
2021
		global $wgContLang, $wgUser, $wgAjaxEditStash;
2022
2023
		if ( is_object( $revision ) ) {
2024
			$revid = $revision->getId();
2025
		} else {
2026
			$revid = $revision;
2027
			// This code path is deprecated, and nothing is known to
2028
			// use it, so performance here shouldn't be a worry.
2029
			if ( $revid !== null ) {
2030
				$revision = Revision::newFromId( $revid, Revision::READ_LATEST );
2031
			} else {
2032
				$revision = null;
2033
			}
2034
		}
2035
2036
		$user = is_null( $user ) ? $wgUser : $user;
2037
		// XXX: check $user->getId() here???
2038
2039
		// Use a sane default for $serialFormat, see bug 57026
2040
		if ( $serialFormat === null ) {
2041
			$serialFormat = $content->getContentHandler()->getDefaultFormat();
2042
		}
2043
2044
		if ( $this->mPreparedEdit
2045
			&& $this->mPreparedEdit->newContent
2046
			&& $this->mPreparedEdit->newContent->equals( $content )
2047
			&& $this->mPreparedEdit->revid == $revid
2048
			&& $this->mPreparedEdit->format == $serialFormat
2049
			// XXX: also check $user here?
2050
		) {
2051
			// Already prepared
2052
			return $this->mPreparedEdit;
2053
		}
2054
2055
		// The edit may have already been prepared via api.php?action=stashedit
2056
		$cachedEdit = $useCache && $wgAjaxEditStash
2057
			? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
2058
			: false;
2059
2060
		$popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2061
		Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
2062
2063
		$edit = (object)[];
2064
		if ( $cachedEdit ) {
2065
			$edit->timestamp = $cachedEdit->timestamp;
2066
		} else {
2067
			$edit->timestamp = wfTimestampNow();
2068
		}
2069
		// @note: $cachedEdit is not used if the rev ID was referenced in the text
2070
		$edit->revid = $revid;
2071
2072
		if ( $cachedEdit ) {
2073
			$edit->pstContent = $cachedEdit->pstContent;
2074
		} else {
2075
			$edit->pstContent = $content
2076
				? $content->preSaveTransform( $this->mTitle, $user, $popts )
2077
				: null;
2078
		}
2079
2080
		$edit->format = $serialFormat;
2081
		$edit->popts = $this->makeParserOptions( 'canonical' );
2082
		if ( $cachedEdit ) {
2083
			$edit->output = $cachedEdit->output;
2084
		} else {
2085
			if ( $revision ) {
2086
				// We get here if vary-revision is set. This means that this page references
2087
				// itself (such as via self-transclusion). In this case, we need to make sure
2088
				// that any such self-references refer to the newly-saved revision, and not
2089
				// to the previous one, which could otherwise happen due to slave lag.
2090
				$oldCallback = $edit->popts->getCurrentRevisionCallback();
2091
				$edit->popts->setCurrentRevisionCallback(
2092
					function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
2093
						if ( $title->equals( $revision->getTitle() ) ) {
2094
							return $revision;
2095
						} else {
2096
							return call_user_func( $oldCallback, $title, $parser );
2097
						}
2098
					}
2099
				);
2100
			}
2101
			$edit->output = $edit->pstContent
2102
				? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2103
				: null;
2104
		}
2105
2106
		$edit->newContent = $content;
2107
		$edit->oldContent = $this->getContent( Revision::RAW );
2108
2109
		// NOTE: B/C for hooks! don't use these fields!
2110
		$edit->newText = $edit->newContent
2111
			? ContentHandler::getContentText( $edit->newContent )
2112
			: '';
2113
		$edit->oldText = $edit->oldContent
2114
			? ContentHandler::getContentText( $edit->oldContent )
2115
			: '';
2116
		$edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
2117
2118
		$this->mPreparedEdit = $edit;
2119
		return $edit;
2120
	}
2121
2122
	/**
2123
	 * Do standard deferred updates after page edit.
2124
	 * Update links tables, site stats, search index and message cache.
2125
	 * Purges pages that include this page if the text was changed here.
2126
	 * Every 100th edit, prune the recent changes table.
2127
	 *
2128
	 * @param Revision $revision
2129
	 * @param User $user User object that did the revision
2130
	 * @param array $options Array of options, following indexes are used:
2131
	 * - changed: boolean, whether the revision changed the content (default true)
2132
	 * - created: boolean, whether the revision created the page (default false)
2133
	 * - moved: boolean, whether the page was moved (default false)
2134
	 * - restored: boolean, whether the page was undeleted (default false)
2135
	 * - oldrevision: Revision object for the pre-update revision (default null)
2136
	 * - oldcountable: boolean, null, or string 'no-change' (default null):
2137
	 *   - boolean: whether the page was counted as an article before that
2138
	 *     revision, only used in changed is true and created is false
2139
	 *   - null: if created is false, don't update the article count; if created
2140
	 *     is true, do update the article count
2141
	 *   - 'no-change': don't update the article count, ever
2142
	 */
2143
	public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
2144
		global $wgRCWatchCategoryMembership, $wgContLang;
2145
2146
		$options += [
2147
			'changed' => true,
2148
			'created' => false,
2149
			'moved' => false,
2150
			'restored' => false,
2151
			'oldrevision' => null,
2152
			'oldcountable' => null
2153
		];
2154
		$content = $revision->getContent();
2155
2156
		// Parse the text
2157
		// Be careful not to do pre-save transform twice: $text is usually
2158
		// already pre-save transformed once.
2159
		if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2160
			wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
2161
			$editInfo = $this->prepareContentForEdit( $content, $revision, $user );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2154 can be null; however, WikiPage::prepareContentForEdit() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2162
		} else {
2163
			wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
2164
			$editInfo = $this->mPreparedEdit;
2165
		}
2166
2167
		// Save it to the parser cache.
2168
		// Make sure the cache time matches page_touched to avoid double parsing.
2169
		ParserCache::singleton()->save(
2170
			$editInfo->output, $this, $editInfo->popts,
2171
			$revision->getTimestamp(), $editInfo->revid
0 ignored issues
show
Security Bug introduced by
It seems like $revision->getTimestamp() targeting Revision::getTimestamp() can also be of type false; however, ParserCache::save() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
2172
		);
2173
2174
		// Update the links tables and other secondary data
2175
		if ( $content ) {
2176
			$recursive = $options['changed']; // bug 50785
2177
			$updates = $content->getSecondaryDataUpdates(
2178
				$this->getTitle(), null, $recursive, $editInfo->output
2179
			);
2180
			foreach ( $updates as $update ) {
2181
				if ( $update instanceof LinksUpdate ) {
2182
					$update->setRevision( $revision );
2183
					$update->setTriggeringUser( $user );
2184
				}
2185
				DeferredUpdates::addUpdate( $update );
2186
			}
2187
			if ( $wgRCWatchCategoryMembership
2188
				&& $this->getContentHandler()->supportsCategories() === true
2189
				&& ( $options['changed'] || $options['created'] )
2190
				&& !$options['restored']
2191
			) {
2192
				// Note: jobs are pushed after deferred updates, so the job should be able to see
2193
				// the recent change entry (also done via deferred updates) and carry over any
2194
				// bot/deletion/IP flags, ect.
2195
				JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
2196
					$this->getTitle(),
2197
					[
2198
						'pageId' => $this->getId(),
2199
						'revTimestamp' => $revision->getTimestamp()
2200
					]
2201
				) );
2202
			}
2203
		}
2204
2205
		Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
2206
2207
		if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
2208
			// Flush old entries from the `recentchanges` table
2209
			if ( mt_rand( 0, 9 ) == 0 ) {
2210
				JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
2211
			}
2212
		}
2213
2214
		if ( !$this->exists() ) {
2215
			return;
2216
		}
2217
2218
		$id = $this->getId();
2219
		$title = $this->mTitle->getPrefixedDBkey();
2220
		$shortTitle = $this->mTitle->getDBkey();
2221
2222
		if ( $options['oldcountable'] === 'no-change' ||
2223
			( !$options['changed'] && !$options['moved'] )
2224
		) {
2225
			$good = 0;
2226
		} elseif ( $options['created'] ) {
2227
			$good = (int)$this->isCountable( $editInfo );
2228
		} elseif ( $options['oldcountable'] !== null ) {
2229
			$good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2230
		} else {
2231
			$good = 0;
2232
		}
2233
		$edits = $options['changed'] ? 1 : 0;
2234
		$total = $options['created'] ? 1 : 0;
2235
2236
		DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2237
		DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
0 ignored issues
show
Bug introduced by
It seems like $content defined by $revision->getContent() on line 2154 can be null; however, SearchUpdate::__construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2238
2239
		// If this is another user's talk page, update newtalk.
2240
		// Don't do this if $options['changed'] = false (null-edits) nor if
2241
		// it's a minor edit and the user doesn't want notifications for those.
2242
		if ( $options['changed']
2243
			&& $this->mTitle->getNamespace() == NS_USER_TALK
2244
			&& $shortTitle != $user->getTitleKey()
2245
			&& !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2246
		) {
2247
			$recipient = User::newFromName( $shortTitle, false );
2248
			if ( !$recipient ) {
2249
				wfDebug( __METHOD__ . ": invalid username\n" );
2250
			} else {
2251
				// Allow extensions to prevent user notification
2252
				// when a new message is added to their talk page
2253
				if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
2254
					if ( User::isIP( $shortTitle ) ) {
2255
						// An anonymous user
2256
						$recipient->setNewtalk( true, $revision );
2257
					} elseif ( $recipient->isLoggedIn() ) {
2258
						$recipient->setNewtalk( true, $revision );
2259
					} else {
2260
						wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2261
					}
2262
				}
2263
			}
2264
		}
2265
2266
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2267
			// XXX: could skip pseudo-messages like js/css here, based on content model.
2268
			$msgtext = $content ? $content->getWikitextForTransclusion() : null;
2269
			if ( $msgtext === false || $msgtext === null ) {
2270
				$msgtext = '';
2271
			}
2272
2273
			MessageCache::singleton()->replace( $shortTitle, $msgtext );
2274
2275
			if ( $wgContLang->hasVariants() ) {
2276
				$wgContLang->updateConversionTable( $this->mTitle );
2277
			}
2278
		}
2279
2280
		if ( $options['created'] ) {
2281
			self::onArticleCreate( $this->mTitle );
2282
		} elseif ( $options['changed'] ) { // bug 50785
2283
			self::onArticleEdit( $this->mTitle, $revision );
2284
		}
2285
	}
2286
2287
	/**
2288
	 * Edit an article without doing all that other stuff
2289
	 * The article must already exist; link tables etc
2290
	 * are not updated, caches are not flushed.
2291
	 *
2292
	 * @param Content $content Content submitted
2293
	 * @param User $user The relevant user
2294
	 * @param string $comment Comment submitted
2295
	 * @param bool $minor Whereas it's a minor modification
2296
	 * @param string $serialFormat Format for storing the content in the database
2297
	 */
2298
	public function doQuickEditContent(
2299
		Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
2300
	) {
2301
2302
		$serialized = $content->serialize( $serialFormat );
2303
2304
		$dbw = wfGetDB( DB_MASTER );
2305
		$revision = new Revision( [
2306
			'title'      => $this->getTitle(), // for determining the default content model
2307
			'page'       => $this->getId(),
2308
			'user_text'  => $user->getName(),
2309
			'user'       => $user->getId(),
2310
			'text'       => $serialized,
2311
			'length'     => $content->getSize(),
2312
			'comment'    => $comment,
2313
			'minor_edit' => $minor ? 1 : 0,
2314
		] ); // XXX: set the content object?
2315
		$revision->insertOn( $dbw );
2316
		$this->updateRevisionOn( $dbw, $revision );
2317
2318
		Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
2319
2320
	}
2321
2322
	/**
2323
	 * Update the article's restriction field, and leave a log entry.
2324
	 * This works for protection both existing and non-existing pages.
2325
	 *
2326
	 * @param array $limit Set of restriction keys
2327
	 * @param array $expiry Per restriction type expiration
2328
	 * @param int &$cascade Set to false if cascading protection isn't allowed.
2329
	 * @param string $reason
2330
	 * @param User $user The user updating the restrictions
2331
	 * @param string|string[] $tags Change tags to add to the pages and protection log entries
2332
	 *   ($user should be able to add the specified tags before this is called)
2333
	 * @return Status Status object; if action is taken, $status->value is the log_id of the
2334
	 *   protection log entry.
2335
	 */
2336
	public function doUpdateRestrictions( array $limit, array $expiry,
2337
		&$cascade, $reason, User $user, $tags = null
2338
	) {
2339
		global $wgCascadingRestrictionLevels, $wgContLang;
2340
2341
		if ( wfReadOnly() ) {
2342
			return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2343
		}
2344
2345
		$this->loadPageData( 'fromdbmaster' );
2346
		$restrictionTypes = $this->mTitle->getRestrictionTypes();
2347
		$id = $this->getId();
2348
2349
		if ( !$cascade ) {
2350
			$cascade = false;
2351
		}
2352
2353
		// Take this opportunity to purge out expired restrictions
2354
		Title::purgeExpiredRestrictions();
2355
2356
		// @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2357
		// we expect a single selection, but the schema allows otherwise.
2358
		$isProtected = false;
2359
		$protect = false;
2360
		$changed = false;
2361
2362
		$dbw = wfGetDB( DB_MASTER );
2363
2364
		foreach ( $restrictionTypes as $action ) {
2365
			if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2366
				$expiry[$action] = 'infinity';
2367
			}
2368
			if ( !isset( $limit[$action] ) ) {
2369
				$limit[$action] = '';
2370
			} elseif ( $limit[$action] != '' ) {
2371
				$protect = true;
2372
			}
2373
2374
			// Get current restrictions on $action
2375
			$current = implode( '', $this->mTitle->getRestrictions( $action ) );
2376
			if ( $current != '' ) {
2377
				$isProtected = true;
2378
			}
2379
2380
			if ( $limit[$action] != $current ) {
2381
				$changed = true;
2382
			} elseif ( $limit[$action] != '' ) {
2383
				// Only check expiry change if the action is actually being
2384
				// protected, since expiry does nothing on an not-protected
2385
				// action.
2386
				if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2387
					$changed = true;
2388
				}
2389
			}
2390
		}
2391
2392
		if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2393
			$changed = true;
2394
		}
2395
2396
		// If nothing has changed, do nothing
2397
		if ( !$changed ) {
2398
			return Status::newGood();
2399
		}
2400
2401
		if ( !$protect ) { // No protection at all means unprotection
2402
			$revCommentMsg = 'unprotectedarticle';
2403
			$logAction = 'unprotect';
2404
		} elseif ( $isProtected ) {
2405
			$revCommentMsg = 'modifiedarticleprotection';
2406
			$logAction = 'modify';
2407
		} else {
2408
			$revCommentMsg = 'protectedarticle';
2409
			$logAction = 'protect';
2410
		}
2411
2412
		// Truncate for whole multibyte characters
2413
		$reason = $wgContLang->truncate( $reason, 255 );
2414
2415
		$logRelationsValues = [];
2416
		$logRelationsField = null;
2417
		$logParamsDetails = [];
2418
2419
		// Null revision (used for change tag insertion)
2420
		$nullRevision = null;
2421
2422
		if ( $id ) { // Protection of existing page
2423
			if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
2424
				return Status::newGood();
2425
			}
2426
2427
			// Only certain restrictions can cascade...
2428
			$editrestriction = isset( $limit['edit'] )
2429
				? [ $limit['edit'] ]
2430
				: $this->mTitle->getRestrictions( 'edit' );
2431
			foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2432
				$editrestriction[$key] = 'editprotected'; // backwards compatibility
2433
			}
2434
			foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2435
				$editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2436
			}
2437
2438
			$cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2439
			foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2440
				$cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2441
			}
2442
			foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2443
				$cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2444
			}
2445
2446
			// The schema allows multiple restrictions
2447
			if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2448
				$cascade = false;
2449
			}
2450
2451
			// insert null revision to identify the page protection change as edit summary
2452
			$latest = $this->getLatest();
2453
			$nullRevision = $this->insertProtectNullRevision(
2454
				$revCommentMsg,
2455
				$limit,
2456
				$expiry,
2457
				$cascade,
2458
				$reason,
2459
				$user
2460
			);
2461
2462
			if ( $nullRevision === null ) {
2463
				return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2464
			}
2465
2466
			$logRelationsField = 'pr_id';
2467
2468
			// Update restrictions table
2469
			foreach ( $limit as $action => $restrictions ) {
2470
				$dbw->delete(
2471
					'page_restrictions',
2472
					[
2473
						'pr_page' => $id,
2474
						'pr_type' => $action
2475
					],
2476
					__METHOD__
2477
				);
2478
				if ( $restrictions != '' ) {
2479
					$cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
2480
					$dbw->insert(
2481
						'page_restrictions',
2482
						[
2483
							'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2484
							'pr_page' => $id,
2485
							'pr_type' => $action,
2486
							'pr_level' => $restrictions,
2487
							'pr_cascade' => $cascadeValue,
2488
							'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2489
						],
2490
						__METHOD__
2491
					);
2492
					$logRelationsValues[] = $dbw->insertId();
2493
					$logParamsDetails[] = [
2494
						'type' => $action,
2495
						'level' => $restrictions,
2496
						'expiry' => $expiry[$action],
2497
						'cascade' => (bool)$cascadeValue,
2498
					];
2499
				}
2500
			}
2501
2502
			// Clear out legacy restriction fields
2503
			$dbw->update(
2504
				'page',
2505
				[ 'page_restrictions' => '' ],
2506
				[ 'page_id' => $id ],
2507
				__METHOD__
2508
			);
2509
2510
			Hooks::run( 'NewRevisionFromEditComplete',
2511
				[ $this, $nullRevision, $latest, $user ] );
2512
			Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
2513
		} else { // Protection of non-existing page (also known as "title protection")
2514
			// Cascade protection is meaningless in this case
2515
			$cascade = false;
2516
2517
			if ( $limit['create'] != '' ) {
2518
				$dbw->replace( 'protected_titles',
2519
					[ [ 'pt_namespace', 'pt_title' ] ],
2520
					[
2521
						'pt_namespace' => $this->mTitle->getNamespace(),
2522
						'pt_title' => $this->mTitle->getDBkey(),
2523
						'pt_create_perm' => $limit['create'],
2524
						'pt_timestamp' => $dbw->timestamp(),
2525
						'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2526
						'pt_user' => $user->getId(),
2527
						'pt_reason' => $reason,
2528
					], __METHOD__
2529
				);
2530
				$logParamsDetails[] = [
2531
					'type' => 'create',
2532
					'level' => $limit['create'],
2533
					'expiry' => $expiry['create'],
2534
				];
2535
			} else {
2536
				$dbw->delete( 'protected_titles',
2537
					[
2538
						'pt_namespace' => $this->mTitle->getNamespace(),
2539
						'pt_title' => $this->mTitle->getDBkey()
2540
					], __METHOD__
2541
				);
2542
			}
2543
		}
2544
2545
		$this->mTitle->flushRestrictions();
2546
		InfoAction::invalidateCache( $this->mTitle );
2547
2548
		if ( $logAction == 'unprotect' ) {
2549
			$params = [];
2550
		} else {
2551
			$protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2552
			$params = [
2553
				'4::description' => $protectDescriptionLog, // parameter for IRC
2554
				'5:bool:cascade' => $cascade,
2555
				'details' => $logParamsDetails, // parameter for localize and api
2556
			];
2557
		}
2558
2559
		// Update the protection log
2560
		$logEntry = new ManualLogEntry( 'protect', $logAction );
2561
		$logEntry->setTarget( $this->mTitle );
2562
		$logEntry->setComment( $reason );
2563
		$logEntry->setPerformer( $user );
2564
		$logEntry->setParameters( $params );
2565
		if ( !is_null( $nullRevision ) ) {
2566
			$logEntry->setAssociatedRevId( $nullRevision->getId() );
2567
		}
2568
		$logEntry->setTags( $tags );
0 ignored issues
show
Bug introduced by
It seems like $tags defined by parameter $tags on line 2337 can also be of type null; however, ManualLogEntry::setTags() does only seem to accept string|array<integer,string>, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

Loading history...
3190
		}
3191
3192
		// raise error, when the edit is an edit without a new version
3193
		$statusRev = isset( $status->value['revision'] )
3194
			? $status->value['revision']
3195
			: null;
3196 View Code Duplication
		if ( !( $statusRev instanceof Revision ) ) {
3197
			$resultDetails = [ 'current' => $current ];
3198
			return [ [ 'alreadyrolled',
3199
					htmlspecialchars( $this->mTitle->getPrefixedText() ),
3200
					htmlspecialchars( $fromP ),
3201
					htmlspecialchars( $current->getUserText() )
3202
			] ];
3203
		}
3204
3205
		$revId = $statusRev->getId();
3206
3207
		Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3208
3209
		$resultDetails = [
3210
			'summary' => $summary,
3211
			'current' => $current,
3212
			'target' => $target,
3213
			'newid' => $revId
3214
		];
3215
3216
		return [];
3217
	}
3218
3219
	/**
3220
	 * The onArticle*() functions are supposed to be a kind of hooks
3221
	 * which should be called whenever any of the specified actions
3222
	 * are done.
3223
	 *
3224
	 * This is a good place to put code to clear caches, for instance.
3225
	 *
3226
	 * This is called on page move and undelete, as well as edit
3227
	 *
3228
	 * @param Title $title
3229
	 */
3230
	public static function onArticleCreate( Title $title ) {
3231
		// Update existence markers on article/talk tabs...
3232
		$other = $title->getOtherPage();
3233
3234
		$other->purgeSquid();
3235
3236
		$title->touchLinks();
3237
		$title->purgeSquid();
3238
		$title->deleteTitleProtection();
3239
	}
3240
3241
	/**
3242
	 * Clears caches when article is deleted
3243
	 *
3244
	 * @param Title $title
3245
	 */
3246
	public static function onArticleDelete( Title $title ) {
3247
		global $wgContLang;
3248
3249
		// Update existence markers on article/talk tabs...
3250
		$other = $title->getOtherPage();
3251
3252
		$other->purgeSquid();
3253
3254
		$title->touchLinks();
3255
		$title->purgeSquid();
3256
3257
		// File cache
3258
		HTMLFileCache::clearFileCache( $title );
3259
		InfoAction::invalidateCache( $title );
3260
3261
		// Messages
3262
		if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3263
			MessageCache::singleton()->replace( $title->getDBkey(), false );
3264
3265
			if ( $wgContLang->hasVariants() ) {
3266
				$wgContLang->updateConversionTable( $title );
3267
			}
3268
		}
3269
3270
		// Images
3271
		if ( $title->getNamespace() == NS_FILE ) {
3272
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
3273
		}
3274
3275
		// User talk pages
3276
		if ( $title->getNamespace() == NS_USER_TALK ) {
3277
			$user = User::newFromName( $title->getText(), false );
3278
			if ( $user ) {
3279
				$user->setNewtalk( false );
3280
			}
3281
		}
3282
3283
		// Image redirects
3284
		RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3285
	}
3286
3287
	/**
3288
	 * Purge caches on page update etc
3289
	 *
3290
	 * @param Title $title
3291
	 * @param Revision|null $revision Revision that was just saved, may be null
3292
	 */
3293
	public static function onArticleEdit( Title $title, Revision $revision = null ) {
3294
		// Invalidate caches of articles which include this page
3295
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
3296
3297
		// Invalidate the caches of all pages which redirect here
3298
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
3299
3300
		// Purge CDN for this page only
3301
		$title->purgeSquid();
3302
		// Clear file cache for this page only
3303
		HTMLFileCache::clearFileCache( $title );
3304
3305
		$revid = $revision ? $revision->getId() : null;
3306
		DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
3307
			InfoAction::invalidateCache( $title, $revid );
3308
		} );
3309
	}
3310
3311
	/**#@-*/
3312
3313
	/**
3314
	 * Returns a list of categories this page is a member of.
3315
	 * Results will include hidden categories
3316
	 *
3317
	 * @return TitleArray
3318
	 */
3319
	public function getCategories() {
3320
		$id = $this->getId();
3321
		if ( $id == 0 ) {
3322
			return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
3323
		}
3324
3325
		$dbr = wfGetDB( DB_SLAVE );
3326
		$res = $dbr->select( 'categorylinks',
3327
			[ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
3328
			// Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
3329
			// as not being aliases, and NS_CATEGORY is numeric
3330
			[ 'cl_from' => $id ],
3331
			__METHOD__ );
3332
3333
		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 3326 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...
3334
	}
3335
3336
	/**
3337
	 * Returns a list of hidden categories this page is a member of.
3338
	 * Uses the page_props and categorylinks tables.
3339
	 *
3340
	 * @return array Array of Title objects
3341
	 */
3342
	public function getHiddenCategories() {
3343
		$result = [];
3344
		$id = $this->getId();
3345
3346
		if ( $id == 0 ) {
3347
			return [];
3348
		}
3349
3350
		$dbr = wfGetDB( DB_SLAVE );
3351
		$res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
3352
			[ 'cl_to' ],
3353
			[ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3354
				'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
3355
			__METHOD__ );
3356
3357
		if ( $res !== false ) {
3358
			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...
3359
				$result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3360
			}
3361
		}
3362
3363
		return $result;
3364
	}
3365
3366
	/**
3367
	 * Return an applicable autosummary if one exists for the given edit.
3368
	 * @param string|null $oldtext The previous text of the page.
3369
	 * @param string|null $newtext The submitted text of the page.
3370
	 * @param int $flags Bitmask: a bitmask of flags submitted for the edit.
3371
	 * @return string An appropriate autosummary, or an empty string.
3372
	 *
3373
	 * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
3374
	 */
3375
	public static function getAutosummary( $oldtext, $newtext, $flags ) {
3376
		// NOTE: stub for backwards-compatibility. assumes the given text is
3377
		// wikitext. will break horribly if it isn't.
3378
3379
		ContentHandler::deprecated( __METHOD__, '1.21' );
3380
3381
		$handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
3382
		$oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3383
		$newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3384
3385
		return $handler->getAutosummary( $oldContent, $newContent, $flags );
3386
	}
3387
3388
	/**
3389
	 * Auto-generates a deletion reason
3390
	 *
3391
	 * @param bool &$hasHistory Whether the page has a history
3392
	 * @return string|bool String containing deletion reason or empty string, or boolean false
3393
	 *    if no revision occurred
3394
	 */
3395
	public function getAutoDeleteReason( &$hasHistory ) {
3396
		return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3397
	}
3398
3399
	/**
3400
	 * Update all the appropriate counts in the category table, given that
3401
	 * we've added the categories $added and deleted the categories $deleted.
3402
	 *
3403
	 * @param array $added The names of categories that were added
3404
	 * @param array $deleted The names of categories that were deleted
3405
	 */
3406
	public function updateCategoryCounts( array $added, array $deleted ) {
3407
		$that = $this;
3408
		$method = __METHOD__;
3409
		$dbw = wfGetDB( DB_MASTER );
3410
3411
		// Do this at the end of the commit to reduce lock wait timeouts
3412
		$dbw->onTransactionPreCommitOrIdle(
3413
			function () use ( $dbw, $that, $method, $added, $deleted ) {
3414
				$ns = $that->getTitle()->getNamespace();
3415
3416
				$addFields = [ 'cat_pages = cat_pages + 1' ];
3417
				$removeFields = [ 'cat_pages = cat_pages - 1' ];
3418
				if ( $ns == NS_CATEGORY ) {
3419
					$addFields[] = 'cat_subcats = cat_subcats + 1';
3420
					$removeFields[] = 'cat_subcats = cat_subcats - 1';
3421
				} elseif ( $ns == NS_FILE ) {
3422
					$addFields[] = 'cat_files = cat_files + 1';
3423
					$removeFields[] = 'cat_files = cat_files - 1';
3424
				}
3425
3426
				if ( count( $added ) ) {
3427
					$existingAdded = $dbw->selectFieldValues(
3428
						'category',
3429
						'cat_title',
3430
						[ 'cat_title' => $added ],
3431
						__METHOD__
3432
					);
3433
3434
					// For category rows that already exist, do a plain
3435
					// UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
3436
					// to avoid creating gaps in the cat_id sequence.
3437
					if ( count( $existingAdded ) ) {
3438
						$dbw->update(
3439
							'category',
3440
							$addFields,
3441
							[ 'cat_title' => $existingAdded ],
3442
							__METHOD__
3443
						);
3444
					}
3445
3446
					$missingAdded = array_diff( $added, $existingAdded );
3447
					if ( count( $missingAdded ) ) {
3448
						$insertRows = [];
3449
						foreach ( $missingAdded as $cat ) {
3450
							$insertRows[] = [
3451
								'cat_title'   => $cat,
3452
								'cat_pages'   => 1,
3453
								'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3454
								'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
3455
							];
3456
						}
3457
						$dbw->upsert(
3458
							'category',
3459
							$insertRows,
3460
							[ 'cat_title' ],
3461
							$addFields,
3462
							$method
3463
						);
3464
					}
3465
				}
3466
3467
				if ( count( $deleted ) ) {
3468
					$dbw->update(
3469
						'category',
3470
						$removeFields,
3471
						[ 'cat_title' => $deleted ],
3472
						$method
3473
					);
3474
				}
3475
3476
				foreach ( $added as $catName ) {
3477
					$cat = Category::newFromName( $catName );
3478
					Hooks::run( 'CategoryAfterPageAdded', [ $cat, $that ] );
3479
				}
3480
3481
				foreach ( $deleted as $catName ) {
3482
					$cat = Category::newFromName( $catName );
3483
					Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $that ] );
3484
				}
3485
			}
3486
		);
3487
	}
3488
3489
	/**
3490
	 * Opportunistically enqueue link update jobs given fresh parser output if useful
3491
	 *
3492
	 * @param ParserOutput $parserOutput Current version page output
3493
	 * @since 1.25
3494
	 */
3495
	public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
3496
		if ( wfReadOnly() ) {
3497
			return;
3498
		}
3499
3500
		if ( !Hooks::run( 'OpportunisticLinksUpdate',
3501
			[ $this, $this->mTitle, $parserOutput ]
3502
		) ) {
3503
			return;
3504
		}
3505
3506
		$params = [
3507
			'isOpportunistic' => true,
3508
			'rootJobTimestamp' => $parserOutput->getCacheTime()
3509
		];
3510
3511
		if ( $this->mTitle->areRestrictionsCascading() ) {
3512
			// If the page is cascade protecting, the links should really be up-to-date
3513
			JobQueueGroup::singleton()->lazyPush(
3514
				RefreshLinksJob::newPrioritized( $this->mTitle, $params )
3515
			);
3516
		} elseif ( $parserOutput->hasDynamicContent() ) {
3517
			// Assume the output contains "dynamic" time/random based magic words.
3518
			// Only update pages that expired due to dynamic content and NOT due to edits
3519
			// to referenced templates/files. When the cache expires due to dynamic content,
3520
			// page_touched is unchanged. We want to avoid triggering redundant jobs due to
3521
			// views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
3522
			// template/file edit already triggered recursive RefreshLinksJob jobs.
3523
			if ( $this->getLinksTimestamp() > $this->getTouched() ) {
3524
				// If a page is uncacheable, do not keep spamming a job for it.
3525
				// Although it would be de-duplicated, it would still waste I/O.
3526
				$cache = ObjectCache::getLocalClusterInstance();
3527
				$key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
3528
				if ( $cache->add( $key, time(), 60 ) ) {
3529
					JobQueueGroup::singleton()->lazyPush(
3530
						RefreshLinksJob::newDynamic( $this->mTitle, $params )
3531
					);
3532
				}
3533
			}
3534
		}
3535
	}
3536
3537
	/**
3538
	 * Returns a list of updates to be performed when this page is deleted. The
3539
	 * updates should remove any information about this page from secondary data
3540
	 * stores such as links tables.
3541
	 *
3542
	 * @param Content|null $content Optional Content object for determining the
3543
	 *   necessary updates.
3544
	 * @return DataUpdate[]
3545
	 */
3546
	public function getDeletionUpdates( Content $content = null ) {
3547
		if ( !$content ) {
3548
			// load content object, which may be used to determine the necessary updates.
3549
			// XXX: the content may not be needed to determine the updates.
3550
			$content = $this->getContent( Revision::RAW );
3551
		}
3552
3553
		if ( !$content ) {
3554
			$updates = [];
3555
		} else {
3556
			$updates = $content->getDeletionUpdates( $this );
3557
		}
3558
3559
		Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3560
		return $updates;
3561
	}
3562
}
3563