Revision::getPreviousRevisionId()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 1
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Representation of a page version.
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
use MediaWiki\Linker\LinkTarget;
23
use MediaWiki\MediaWikiServices;
24
25
/**
26
 * @todo document
27
 */
28
class Revision implements IDBAccessObject {
29
	/** @var int|null */
30
	protected $mId;
31
	/** @var int|null */
32
	protected $mPage;
33
	/** @var string */
34
	protected $mUserText;
35
	/** @var string */
36
	protected $mOrigUserText;
37
	/** @var int */
38
	protected $mUser;
39
	/** @var bool */
40
	protected $mMinorEdit;
41
	/** @var string */
42
	protected $mTimestamp;
43
	/** @var int */
44
	protected $mDeleted;
45
	/** @var int */
46
	protected $mSize;
47
	/** @var string */
48
	protected $mSha1;
49
	/** @var int */
50
	protected $mParentId;
51
	/** @var string */
52
	protected $mComment;
53
	/** @var string */
54
	protected $mText;
55
	/** @var int */
56
	protected $mTextId;
57
	/** @var int */
58
	protected $mUnpatrolled;
59
60
	/** @var stdClass|null */
61
	protected $mTextRow;
62
63
	/**  @var null|Title */
64
	protected $mTitle;
65
	/** @var bool */
66
	protected $mCurrent;
67
	/** @var string */
68
	protected $mContentModel;
69
	/** @var string */
70
	protected $mContentFormat;
71
72
	/** @var Content|null|bool */
73
	protected $mContent;
74
	/** @var null|ContentHandler */
75
	protected $mContentHandler;
76
77
	/** @var int */
78
	protected $mQueryFlags = 0;
79
	/** @var bool Used for cached values to reload user text and rev_deleted */
80
	protected $mRefreshMutableFields = false;
81
	/** @var string Wiki ID; false means the current wiki */
82
	protected $mWiki = false;
83
84
	// Revision deletion constants
85
	const DELETED_TEXT = 1;
86
	const DELETED_COMMENT = 2;
87
	const DELETED_USER = 4;
88
	const DELETED_RESTRICTED = 8;
89
	const SUPPRESSED_USER = 12; // convenience
90
	const SUPPRESSED_ALL = 15; // convenience
91
92
	// Audience options for accessors
93
	const FOR_PUBLIC = 1;
94
	const FOR_THIS_USER = 2;
95
	const RAW = 3;
96
97
	const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
98
99
	/**
100
	 * Load a page revision from a given revision ID number.
101
	 * Returns null if no such revision can be found.
102
	 *
103
	 * $flags include:
104
	 *      Revision::READ_LATEST  : Select the data from the master
105
	 *      Revision::READ_LOCKING : Select & lock the data from the master
106
	 *
107
	 * @param int $id
108
	 * @param int $flags (optional)
109
	 * @return Revision|null
110
	 */
111
	public static function newFromId( $id, $flags = 0 ) {
112
		return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
113
	}
114
115
	/**
116
	 * Load either the current, or a specified, revision
117
	 * that's attached to a given link target. If not attached
118
	 * to that link target, will return null.
119
	 *
120
	 * $flags include:
121
	 *      Revision::READ_LATEST  : Select the data from the master
122
	 *      Revision::READ_LOCKING : Select & lock the data from the master
123
	 *
124
	 * @param LinkTarget $linkTarget
125
	 * @param int $id (optional)
126
	 * @param int $flags Bitfield (optional)
127
	 * @return Revision|null
128
	 */
129 View Code Duplication
	public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
130
		$conds = [
131
			'page_namespace' => $linkTarget->getNamespace(),
132
			'page_title' => $linkTarget->getDBkey()
133
		];
134
		if ( $id ) {
135
			// Use the specified ID
136
			$conds['rev_id'] = $id;
137
			return self::newFromConds( $conds, $flags );
138
		} else {
139
			// Use a join to get the latest revision
140
			$conds[] = 'rev_id=page_latest';
141
			$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
142
			return self::loadFromConds( $db, $conds, $flags );
0 ignored issues
show
Bug introduced by
It seems like $db defined by wfGetDB($flags & self::R...DB_MASTER : DB_REPLICA) on line 141 can be null; however, Revision::loadFromConds() 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...
143
		}
144
	}
145
146
	/**
147
	 * Load either the current, or a specified, revision
148
	 * that's attached to a given page ID.
149
	 * Returns null if no such revision can be found.
150
	 *
151
	 * $flags include:
152
	 *      Revision::READ_LATEST  : Select the data from the master (since 1.20)
153
	 *      Revision::READ_LOCKING : Select & lock the data from the master
154
	 *
155
	 * @param int $pageId
156
	 * @param int $revId (optional)
157
	 * @param int $flags Bitfield (optional)
158
	 * @return Revision|null
159
	 */
160 View Code Duplication
	public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
161
		$conds = [ 'page_id' => $pageId ];
162
		if ( $revId ) {
163
			$conds['rev_id'] = $revId;
164
			return self::newFromConds( $conds, $flags );
165
		} else {
166
			// Use a join to get the latest revision
167
			$conds[] = 'rev_id = page_latest';
168
			$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
169
			return self::loadFromConds( $db, $conds, $flags );
0 ignored issues
show
Bug introduced by
It seems like $db defined by wfGetDB($flags & self::R...DB_MASTER : DB_REPLICA) on line 168 can be null; however, Revision::loadFromConds() 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...
170
		}
171
	}
172
173
	/**
174
	 * Make a fake revision object from an archive table row. This is queried
175
	 * for permissions or even inserted (as in Special:Undelete)
176
	 * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
177
	 *
178
	 * @param object $row
179
	 * @param array $overrides
180
	 *
181
	 * @throws MWException
182
	 * @return Revision
183
	 */
184
	public static function newFromArchiveRow( $row, $overrides = [] ) {
185
		global $wgContentHandlerUseDB;
186
187
		$attribs = $overrides + [
188
			'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
189
			'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
190
			'comment'    => $row->ar_comment,
191
			'user'       => $row->ar_user,
192
			'user_text'  => $row->ar_user_text,
193
			'timestamp'  => $row->ar_timestamp,
194
			'minor_edit' => $row->ar_minor_edit,
195
			'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
196
			'deleted'    => $row->ar_deleted,
197
			'len'        => $row->ar_len,
198
			'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
199
			'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
200
			'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
201
		];
202
203
		if ( !$wgContentHandlerUseDB ) {
204
			unset( $attribs['content_model'] );
205
			unset( $attribs['content_format'] );
206
		}
207
208
		if ( !isset( $attribs['title'] )
209
			&& isset( $row->ar_namespace )
210
			&& isset( $row->ar_title )
211
		) {
212
			$attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
213
		}
214
215
		if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
216
			// Pre-1.5 ar_text row
217
			$attribs['text'] = self::getRevisionText( $row, 'ar_' );
218
			if ( $attribs['text'] === false ) {
219
				throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
220
			}
221
		}
222
		return new self( $attribs );
223
	}
224
225
	/**
226
	 * @since 1.19
227
	 *
228
	 * @param object $row
229
	 * @return Revision
230
	 */
231
	public static function newFromRow( $row ) {
232
		return new self( $row );
233
	}
234
235
	/**
236
	 * Load a page revision from a given revision ID number.
237
	 * Returns null if no such revision can be found.
238
	 *
239
	 * @param IDatabase $db
240
	 * @param int $id
241
	 * @return Revision|null
242
	 */
243
	public static function loadFromId( $db, $id ) {
244
		return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
245
	}
246
247
	/**
248
	 * Load either the current, or a specified, revision
249
	 * that's attached to a given page. If not attached
250
	 * to that page, will return null.
251
	 *
252
	 * @param IDatabase $db
253
	 * @param int $pageid
254
	 * @param int $id
255
	 * @return Revision|null
256
	 */
257
	public static function loadFromPageId( $db, $pageid, $id = 0 ) {
258
		$conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
259
		if ( $id ) {
260
			$conds['rev_id'] = intval( $id );
261
		} else {
262
			$conds[] = 'rev_id=page_latest';
263
		}
264
		return self::loadFromConds( $db, $conds );
265
	}
266
267
	/**
268
	 * Load either the current, or a specified, revision
269
	 * that's attached to a given page. If not attached
270
	 * to that page, will return null.
271
	 *
272
	 * @param IDatabase $db
273
	 * @param Title $title
274
	 * @param int $id
275
	 * @return Revision|null
276
	 */
277
	public static function loadFromTitle( $db, $title, $id = 0 ) {
278
		if ( $id ) {
279
			$matchId = intval( $id );
280
		} else {
281
			$matchId = 'page_latest';
282
		}
283
		return self::loadFromConds( $db,
284
			[
285
				"rev_id=$matchId",
286
				'page_namespace' => $title->getNamespace(),
287
				'page_title' => $title->getDBkey()
288
			]
289
		);
290
	}
291
292
	/**
293
	 * Load the revision for the given title with the given timestamp.
294
	 * WARNING: Timestamps may in some circumstances not be unique,
295
	 * so this isn't the best key to use.
296
	 *
297
	 * @param IDatabase $db
298
	 * @param Title $title
299
	 * @param string $timestamp
300
	 * @return Revision|null
301
	 */
302
	public static function loadFromTimestamp( $db, $title, $timestamp ) {
303
		return self::loadFromConds( $db,
304
			[
305
				'rev_timestamp' => $db->timestamp( $timestamp ),
306
				'page_namespace' => $title->getNamespace(),
307
				'page_title' => $title->getDBkey()
308
			]
309
		);
310
	}
311
312
	/**
313
	 * Given a set of conditions, fetch a revision
314
	 *
315
	 * This method is used then a revision ID is qualified and
316
	 * will incorporate some basic replica DB/master fallback logic
317
	 *
318
	 * @param array $conditions
319
	 * @param int $flags (optional)
320
	 * @return Revision|null
321
	 */
322
	private static function newFromConds( $conditions, $flags = 0 ) {
323
		$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
324
325
		$rev = self::loadFromConds( $db, $conditions, $flags );
0 ignored issues
show
Bug introduced by
It seems like $db defined by wfGetDB($flags & self::R...DB_MASTER : DB_REPLICA) on line 323 can be null; however, Revision::loadFromConds() 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...
326
		// Make sure new pending/committed revision are visibile later on
327
		// within web requests to certain avoid bugs like T93866 and T94407.
328
		if ( !$rev
329
			&& !( $flags & self::READ_LATEST )
330
			&& wfGetLB()->getServerCount() > 1
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

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

Loading history...
331
			&& wfGetLB()->hasOrMadeRecentMasterChanges()
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

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

Loading history...
332
		) {
333
			$flags = self::READ_LATEST;
334
			$db = wfGetDB( DB_MASTER );
335
			$rev = self::loadFromConds( $db, $conditions, $flags );
0 ignored issues
show
Bug introduced by
It seems like $db defined by wfGetDB(DB_MASTER) on line 334 can be null; however, Revision::loadFromConds() 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...
336
		}
337
338
		if ( $rev ) {
339
			$rev->mQueryFlags = $flags;
340
		}
341
342
		return $rev;
343
	}
344
345
	/**
346
	 * Given a set of conditions, fetch a revision from
347
	 * the given database connection.
348
	 *
349
	 * @param IDatabase $db
350
	 * @param array $conditions
351
	 * @param int $flags (optional)
352
	 * @return Revision|null
353
	 */
354
	private static function loadFromConds( $db, $conditions, $flags = 0 ) {
355
		$row = self::fetchFromConds( $db, $conditions, $flags );
356
		if ( $row ) {
357
			$rev = new Revision( $row );
358
			$rev->mWiki = $db->getWikiID();
359
360
			return $rev;
361
		}
362
363
		return null;
364
	}
365
366
	/**
367
	 * Return a wrapper for a series of database rows to
368
	 * fetch all of a given page's revisions in turn.
369
	 * Each row can be fed to the constructor to get objects.
370
	 *
371
	 * @param LinkTarget $title
372
	 * @return ResultWrapper
373
	 * @deprecated Since 1.28
374
	 */
375
	public static function fetchRevision( LinkTarget $title ) {
376
		$row = self::fetchFromConds(
377
			wfGetDB( DB_REPLICA ),
0 ignored issues
show
Bug introduced by
It seems like wfGetDB(DB_REPLICA) can be null; however, fetchFromConds() 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...
378
			[
379
				'rev_id=page_latest',
380
				'page_namespace' => $title->getNamespace(),
381
				'page_title' => $title->getDBkey()
382
			]
383
		);
384
385
		return new FakeResultWrapper( $row ? [ $row ] : [] );
386
	}
387
388
	/**
389
	 * Given a set of conditions, return a ResultWrapper
390
	 * which will return matching database rows with the
391
	 * fields necessary to build Revision objects.
392
	 *
393
	 * @param IDatabase $db
394
	 * @param array $conditions
395
	 * @param int $flags (optional)
396
	 * @return stdClass
397
	 */
398
	private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
399
		$fields = array_merge(
400
			self::selectFields(),
401
			self::selectPageFields(),
402
			self::selectUserFields()
403
		);
404
		$options = [];
405
		if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
406
			$options[] = 'FOR UPDATE';
407
		}
408
		return $db->selectRow(
409
			[ 'revision', 'page', 'user' ],
410
			$fields,
411
			$conditions,
412
			__METHOD__,
413
			$options,
414
			[ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ]
415
		);
416
	}
417
418
	/**
419
	 * Return the value of a select() JOIN conds array for the user table.
420
	 * This will get user table rows for logged-in users.
421
	 * @since 1.19
422
	 * @return array
423
	 */
424
	public static function userJoinCond() {
425
		return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
426
	}
427
428
	/**
429
	 * Return the value of a select() page conds array for the page table.
430
	 * This will assure that the revision(s) are not orphaned from live pages.
431
	 * @since 1.19
432
	 * @return array
433
	 */
434
	public static function pageJoinCond() {
435
		return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
436
	}
437
438
	/**
439
	 * Return the list of revision fields that should be selected to create
440
	 * a new revision.
441
	 * @return array
442
	 */
443
	public static function selectFields() {
444
		global $wgContentHandlerUseDB;
445
446
		$fields = [
447
			'rev_id',
448
			'rev_page',
449
			'rev_text_id',
450
			'rev_timestamp',
451
			'rev_comment',
452
			'rev_user_text',
453
			'rev_user',
454
			'rev_minor_edit',
455
			'rev_deleted',
456
			'rev_len',
457
			'rev_parent_id',
458
			'rev_sha1',
459
		];
460
461
		if ( $wgContentHandlerUseDB ) {
462
			$fields[] = 'rev_content_format';
463
			$fields[] = 'rev_content_model';
464
		}
465
466
		return $fields;
467
	}
468
469
	/**
470
	 * Return the list of revision fields that should be selected to create
471
	 * a new revision from an archive row.
472
	 * @return array
473
	 */
474
	public static function selectArchiveFields() {
475
		global $wgContentHandlerUseDB;
476
		$fields = [
477
			'ar_id',
478
			'ar_page_id',
479
			'ar_rev_id',
480
			'ar_text',
481
			'ar_text_id',
482
			'ar_timestamp',
483
			'ar_comment',
484
			'ar_user_text',
485
			'ar_user',
486
			'ar_minor_edit',
487
			'ar_deleted',
488
			'ar_len',
489
			'ar_parent_id',
490
			'ar_sha1',
491
		];
492
493
		if ( $wgContentHandlerUseDB ) {
494
			$fields[] = 'ar_content_format';
495
			$fields[] = 'ar_content_model';
496
		}
497
		return $fields;
498
	}
499
500
	/**
501
	 * Return the list of text fields that should be selected to read the
502
	 * revision text
503
	 * @return array
504
	 */
505
	public static function selectTextFields() {
506
		return [
507
			'old_text',
508
			'old_flags'
509
		];
510
	}
511
512
	/**
513
	 * Return the list of page fields that should be selected from page table
514
	 * @return array
515
	 */
516
	public static function selectPageFields() {
517
		return [
518
			'page_namespace',
519
			'page_title',
520
			'page_id',
521
			'page_latest',
522
			'page_is_redirect',
523
			'page_len',
524
		];
525
	}
526
527
	/**
528
	 * Return the list of user fields that should be selected from user table
529
	 * @return array
530
	 */
531
	public static function selectUserFields() {
532
		return [ 'user_name' ];
533
	}
534
535
	/**
536
	 * Do a batched query to get the parent revision lengths
537
	 * @param IDatabase $db
538
	 * @param array $revIds
539
	 * @return array
540
	 */
541
	public static function getParentLengths( $db, array $revIds ) {
542
		$revLens = [];
543
		if ( !$revIds ) {
544
			return $revLens; // empty
545
		}
546
		$res = $db->select( 'revision',
547
			[ 'rev_id', 'rev_len' ],
548
			[ 'rev_id' => $revIds ],
549
			__METHOD__ );
550
		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...
551
			$revLens[$row->rev_id] = $row->rev_len;
552
		}
553
		return $revLens;
554
	}
555
556
	/**
557
	 * Constructor
558
	 *
559
	 * @param object|array $row Either a database row or an array
560
	 * @throws MWException
561
	 * @access private
562
	 */
563
	function __construct( $row ) {
564
		if ( is_object( $row ) ) {
565
			$this->mId = intval( $row->rev_id );
566
			$this->mPage = intval( $row->rev_page );
567
			$this->mTextId = intval( $row->rev_text_id );
568
			$this->mComment = $row->rev_comment;
569
			$this->mUser = intval( $row->rev_user );
570
			$this->mMinorEdit = intval( $row->rev_minor_edit );
0 ignored issues
show
Documentation Bug introduced by
The property $mMinorEdit was declared of type boolean, but intval($row->rev_minor_edit) 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...
571
			$this->mTimestamp = $row->rev_timestamp;
572
			$this->mDeleted = intval( $row->rev_deleted );
573
574
			if ( !isset( $row->rev_parent_id ) ) {
575
				$this->mParentId = null;
576
			} else {
577
				$this->mParentId = intval( $row->rev_parent_id );
578
			}
579
580
			if ( !isset( $row->rev_len ) ) {
581
				$this->mSize = null;
582
			} else {
583
				$this->mSize = intval( $row->rev_len );
584
			}
585
586
			if ( !isset( $row->rev_sha1 ) ) {
587
				$this->mSha1 = null;
588
			} else {
589
				$this->mSha1 = $row->rev_sha1;
590
			}
591
592
			if ( isset( $row->page_latest ) ) {
593
				$this->mCurrent = ( $row->rev_id == $row->page_latest );
594
				$this->mTitle = Title::newFromRow( $row );
595
			} else {
596
				$this->mCurrent = false;
597
				$this->mTitle = null;
598
			}
599
600
			if ( !isset( $row->rev_content_model ) ) {
601
				$this->mContentModel = null; # determine on demand if needed
602
			} else {
603
				$this->mContentModel = strval( $row->rev_content_model );
604
			}
605
606
			if ( !isset( $row->rev_content_format ) ) {
607
				$this->mContentFormat = null; # determine on demand if needed
608
			} else {
609
				$this->mContentFormat = strval( $row->rev_content_format );
610
			}
611
612
			// Lazy extraction...
613
			$this->mText = null;
614
			if ( isset( $row->old_text ) ) {
615
				$this->mTextRow = $row;
616
			} else {
617
				// 'text' table row entry will be lazy-loaded
618
				$this->mTextRow = null;
619
			}
620
621
			// Use user_name for users and rev_user_text for IPs...
622
			$this->mUserText = null; // lazy load if left null
623
			if ( $this->mUser == 0 ) {
624
				$this->mUserText = $row->rev_user_text; // IP user
625
			} elseif ( isset( $row->user_name ) ) {
626
				$this->mUserText = $row->user_name; // logged-in user
627
			}
628
			$this->mOrigUserText = $row->rev_user_text;
629
		} elseif ( is_array( $row ) ) {
630
			// Build a new revision to be saved...
631
			global $wgUser; // ugh
632
633
			# if we have a content object, use it to set the model and type
634
			if ( !empty( $row['content'] ) ) {
635
				// @todo when is that set? test with external store setup! check out insertOn() [dk]
636
				if ( !empty( $row['text_id'] ) ) {
637
					throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
638
						"can't serialize content object" );
639
				}
640
641
				$row['content_model'] = $row['content']->getModel();
642
				# note: mContentFormat is initializes later accordingly
643
				# note: content is serialized later in this method!
644
				# also set text to null?
645
			}
646
647
			$this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
648
			$this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
649
			$this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
650
			$this->mUserText = isset( $row['user_text'] )
651
				? strval( $row['user_text'] ) : $wgUser->getName();
652
			$this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
653
			$this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
0 ignored issues
show
Documentation Bug introduced by
The property $mMinorEdit was declared of type boolean, but isset($row['minor_edit']...$row['minor_edit']) : 0 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...
654
			$this->mTimestamp = isset( $row['timestamp'] )
0 ignored issues
show
Documentation Bug introduced by
It seems like isset($row['timestamp'])...p']) : wfTimestampNow() 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...
655
				? strval( $row['timestamp'] ) : wfTimestampNow();
656
			$this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
657
			$this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
658
			$this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
659
			$this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
660
661
			$this->mContentModel = isset( $row['content_model'] )
662
				? strval( $row['content_model'] ) : null;
663
			$this->mContentFormat = isset( $row['content_format'] )
664
				? strval( $row['content_format'] ) : null;
665
666
			// Enforce spacing trimming on supplied text
667
			$this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
668
			$this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
669
			$this->mTextRow = null;
670
671
			$this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
672
673
			// if we have a Content object, override mText and mContentModel
674
			if ( !empty( $row['content'] ) ) {
675
				if ( !( $row['content'] instanceof Content ) ) {
676
					throw new MWException( '`content` field must contain a Content object.' );
677
				}
678
679
				$handler = $this->getContentHandler();
680
				$this->mContent = $row['content'];
681
682
				$this->mContentModel = $this->mContent->getModel();
683
				$this->mContentHandler = null;
684
685
				$this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
686
			} elseif ( $this->mText !== null ) {
687
				$handler = $this->getContentHandler();
688
				$this->mContent = $handler->unserializeContent( $this->mText );
689
			}
690
691
			// If we have a Title object, make sure it is consistent with mPage.
692
			if ( $this->mTitle && $this->mTitle->exists() ) {
693
				if ( $this->mPage === null ) {
694
					// if the page ID wasn't known, set it now
695
					$this->mPage = $this->mTitle->getArticleID();
696
				} elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
697
					// Got different page IDs. This may be legit (e.g. during undeletion),
698
					// but it seems worth mentioning it in the log.
699
					wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
700
						$this->mTitle->getArticleID() . " provided by the Title object." );
701
				}
702
			}
703
704
			$this->mCurrent = false;
705
706
			// If we still have no length, see it we have the text to figure it out
707
			if ( !$this->mSize && $this->mContent !== null ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mSize 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...
708
				$this->mSize = $this->mContent->getSize();
709
			}
710
711
			// Same for sha1
712
			if ( $this->mSha1 === null ) {
713
				$this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->mText === null ? ...ase36Sha1($this->mText) can also be of type false. However, the property $mSha1 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...
714
			}
715
716
			// force lazy init
717
			$this->getContentModel();
718
			$this->getContentFormat();
719
		} else {
720
			throw new MWException( 'Revision constructor passed invalid row format.' );
721
		}
722
		$this->mUnpatrolled = null;
723
	}
724
725
	/**
726
	 * Get revision ID
727
	 *
728
	 * @return int|null
729
	 */
730
	public function getId() {
731
		return $this->mId;
732
	}
733
734
	/**
735
	 * Set the revision ID
736
	 *
737
	 * This should only be used for proposed revisions that turn out to be null edits
738
	 *
739
	 * @since 1.19
740
	 * @param int $id
741
	 */
742
	public function setId( $id ) {
743
		$this->mId = (int)$id;
744
	}
745
746
	/**
747
	 * Set the user ID/name
748
	 *
749
	 * This should only be used for proposed revisions that turn out to be null edits
750
	 *
751
	 * @since 1.28
752
	 * @param integer $id User ID
753
	 * @param string $name User name
754
	 */
755
	public function setUserIdAndName( $id, $name ) {
756
		$this->mUser = (int)$id;
757
		$this->mUserText = $name;
758
		$this->mOrigUserText = $name;
759
	}
760
761
	/**
762
	 * Get text row ID
763
	 *
764
	 * @return int|null
765
	 */
766
	public function getTextId() {
767
		return $this->mTextId;
768
	}
769
770
	/**
771
	 * Get parent revision ID (the original previous page revision)
772
	 *
773
	 * @return int|null
774
	 */
775
	public function getParentId() {
776
		return $this->mParentId;
777
	}
778
779
	/**
780
	 * Returns the length of the text in this revision, or null if unknown.
781
	 *
782
	 * @return int|null
783
	 */
784
	public function getSize() {
785
		return $this->mSize;
786
	}
787
788
	/**
789
	 * Returns the base36 sha1 of the text in this revision, or null if unknown.
790
	 *
791
	 * @return string|null
792
	 */
793
	public function getSha1() {
794
		return $this->mSha1;
795
	}
796
797
	/**
798
	 * Returns the title of the page associated with this entry or null.
799
	 *
800
	 * Will do a query, when title is not set and id is given.
801
	 *
802
	 * @return Title|null
803
	 */
804
	public function getTitle() {
805
		if ( $this->mTitle !== null ) {
806
			return $this->mTitle;
807
		}
808
		// rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
809
		if ( $this->mId !== null ) {
810
			$dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

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

Loading history...
811
			$row = $dbr->selectRow(
812
				[ 'page', 'revision' ],
813
				self::selectPageFields(),
814
				[ 'page_id=rev_page', 'rev_id' => $this->mId ],
815
				__METHOD__
816
			);
817
			if ( $row ) {
818
				// @TODO: better foreign title handling
819
				$this->mTitle = Title::newFromRow( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow(array('p...this->mId), __METHOD__) on line 811 can also be of type boolean; however, Title::newFromRow() does only seem to accept object<stdClass>, 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...
820
			}
821
		}
822
823
		if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
824
			// Loading by ID is best, though not possible for foreign titles
825
			if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
826
				$this->mTitle = Title::newFromID( $this->mPage );
827
			}
828
		}
829
830
		return $this->mTitle;
831
	}
832
833
	/**
834
	 * Set the title of the revision
835
	 *
836
	 * @param Title $title
837
	 */
838
	public function setTitle( $title ) {
839
		$this->mTitle = $title;
840
	}
841
842
	/**
843
	 * Get the page ID
844
	 *
845
	 * @return int|null
846
	 */
847
	public function getPage() {
848
		return $this->mPage;
849
	}
850
851
	/**
852
	 * Fetch revision's user id if it's available to the specified audience.
853
	 * If the specified audience does not have access to it, zero will be
854
	 * returned.
855
	 *
856
	 * @param int $audience One of:
857
	 *   Revision::FOR_PUBLIC       to be displayed to all users
858
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
859
	 *   Revision::RAW              get the ID regardless of permissions
860
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
861
	 *   to the $audience parameter
862
	 * @return int
863
	 */
864 View Code Duplication
	public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
865
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
866
			return 0;
867
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
868
			return 0;
869
		} else {
870
			return $this->mUser;
871
		}
872
	}
873
874
	/**
875
	 * Fetch revision's user id without regard for the current user's permissions
876
	 *
877
	 * @return string
878
	 * @deprecated since 1.25, use getUser( Revision::RAW )
879
	 */
880
	public function getRawUser() {
881
		wfDeprecated( __METHOD__, '1.25' );
882
		return $this->getUser( self::RAW );
883
	}
884
885
	/**
886
	 * Fetch revision's username if it's available to the specified audience.
887
	 * If the specified audience does not have access to the username, an
888
	 * empty string will be returned.
889
	 *
890
	 * @param int $audience One of:
891
	 *   Revision::FOR_PUBLIC       to be displayed to all users
892
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
893
	 *   Revision::RAW              get the text regardless of permissions
894
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
895
	 *   to the $audience parameter
896
	 * @return string
897
	 */
898
	public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
899
		$this->loadMutableFields();
900
901
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
902
			return '';
903
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
904
			return '';
905
		} else {
906
			if ( $this->mUserText === null ) {
907
				$this->mUserText = User::whoIs( $this->mUser ); // load on demand
0 ignored issues
show
Documentation Bug introduced by
It seems like \User::whoIs($this->mUser) can also be of type boolean. However, the property $mUserText 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...
908
				if ( $this->mUserText === false ) {
909
					# This shouldn't happen, but it can if the wiki was recovered
910
					# via importing revs and there is no user table entry yet.
911
					$this->mUserText = $this->mOrigUserText;
912
				}
913
			}
914
			return $this->mUserText;
915
		}
916
	}
917
918
	/**
919
	 * Fetch revision's username without regard for view restrictions
920
	 *
921
	 * @return string
922
	 * @deprecated since 1.25, use getUserText( Revision::RAW )
923
	 */
924
	public function getRawUserText() {
925
		wfDeprecated( __METHOD__, '1.25' );
926
		return $this->getUserText( self::RAW );
927
	}
928
929
	/**
930
	 * Fetch revision comment if it's available to the specified audience.
931
	 * If the specified audience does not have access to the comment, an
932
	 * empty string will be returned.
933
	 *
934
	 * @param int $audience One of:
935
	 *   Revision::FOR_PUBLIC       to be displayed to all users
936
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
937
	 *   Revision::RAW              get the text regardless of permissions
938
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
939
	 *   to the $audience parameter
940
	 * @return string
941
	 */
942 View Code Duplication
	function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
943
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
944
			return '';
945
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
946
			return '';
947
		} else {
948
			return $this->mComment;
949
		}
950
	}
951
952
	/**
953
	 * Fetch revision comment without regard for the current user's permissions
954
	 *
955
	 * @return string
956
	 * @deprecated since 1.25, use getComment( Revision::RAW )
957
	 */
958
	public function getRawComment() {
959
		wfDeprecated( __METHOD__, '1.25' );
960
		return $this->getComment( self::RAW );
961
	}
962
963
	/**
964
	 * @return bool
965
	 */
966
	public function isMinor() {
967
		return (bool)$this->mMinorEdit;
968
	}
969
970
	/**
971
	 * @return int Rcid of the unpatrolled row, zero if there isn't one
972
	 */
973
	public function isUnpatrolled() {
974
		if ( $this->mUnpatrolled !== null ) {
975
			return $this->mUnpatrolled;
976
		}
977
		$rc = $this->getRecentChange();
978
		if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
979
			$this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
980
		} else {
981
			$this->mUnpatrolled = 0;
982
		}
983
		return $this->mUnpatrolled;
984
	}
985
986
	/**
987
	 * Get the RC object belonging to the current revision, if there's one
988
	 *
989
	 * @param int $flags (optional) $flags include:
990
	 *      Revision::READ_LATEST  : Select the data from the master
991
	 *
992
	 * @since 1.22
993
	 * @return RecentChange|null
994
	 */
995
	public function getRecentChange( $flags = 0 ) {
996
		$dbr = wfGetDB( DB_REPLICA );
997
998
		list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
999
1000
		return RecentChange::newFromConds(
1001
			[
1002
				'rc_user_text' => $this->getUserText( Revision::RAW ),
1003
				'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
1004
				'rc_this_oldid' => $this->getId()
1005
			],
1006
			__METHOD__,
1007
			$dbType
1008
		);
1009
	}
1010
1011
	/**
1012
	 * @param int $field One of DELETED_* bitfield constants
1013
	 *
1014
	 * @return bool
1015
	 */
1016
	public function isDeleted( $field ) {
1017
		if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
1018
			// Current revisions of pages cannot have the content hidden. Skipping this
1019
			// check is very useful for Parser as it fetches templates using newKnownCurrent().
1020
			// Calling getVisibility() in that case triggers a verification database query.
1021
			return false; // no need to check
1022
		}
1023
1024
		return ( $this->getVisibility() & $field ) == $field;
1025
	}
1026
1027
	/**
1028
	 * Get the deletion bitfield of the revision
1029
	 *
1030
	 * @return int
1031
	 */
1032
	public function getVisibility() {
1033
		$this->loadMutableFields();
1034
1035
		return (int)$this->mDeleted;
1036
	}
1037
1038
	/**
1039
	 * Fetch revision text if it's available to the specified audience.
1040
	 * If the specified audience does not have the ability to view this
1041
	 * revision, an empty string will be returned.
1042
	 *
1043
	 * @param int $audience One of:
1044
	 *   Revision::FOR_PUBLIC       to be displayed to all users
1045
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
1046
	 *   Revision::RAW              get the text regardless of permissions
1047
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1048
	 *   to the $audience parameter
1049
	 *
1050
	 * @deprecated since 1.21, use getContent() instead
1051
	 * @return string
1052
	 */
1053
	public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
1054
		wfDeprecated( __METHOD__, '1.21' );
1055
1056
		$content = $this->getContent( $audience, $user );
1057
		return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
1058
	}
1059
1060
	/**
1061
	 * Fetch revision content if it's available to the specified audience.
1062
	 * If the specified audience does not have the ability to view this
1063
	 * revision, null will be returned.
1064
	 *
1065
	 * @param int $audience One of:
1066
	 *   Revision::FOR_PUBLIC       to be displayed to all users
1067
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
1068
	 *   Revision::RAW              get the text regardless of permissions
1069
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1070
	 *   to the $audience parameter
1071
	 * @since 1.21
1072
	 * @return Content|null
1073
	 */
1074
	public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
1075
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
1076
			return null;
1077
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
1078
			return null;
1079
		} else {
1080
			return $this->getContentInternal();
1081
		}
1082
	}
1083
1084
	/**
1085
	 * Get original serialized data (without checking view restrictions)
1086
	 *
1087
	 * @since 1.21
1088
	 * @return string
1089
	 */
1090
	public function getSerializedData() {
1091
		if ( $this->mText === null ) {
1092
			// Revision is immutable. Load on demand.
1093
			$this->mText = $this->loadText();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->loadText() can also be of type boolean. However, the property $mText 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...
1094
		}
1095
1096
		return $this->mText;
1097
	}
1098
1099
	/**
1100
	 * Gets the content object for the revision (or null on failure).
1101
	 *
1102
	 * Note that for mutable Content objects, each call to this method will return a
1103
	 * fresh clone.
1104
	 *
1105
	 * @since 1.21
1106
	 * @return Content|null The Revision's content, or null on failure.
1107
	 */
1108
	protected function getContentInternal() {
1109
		if ( $this->mContent === null ) {
1110
			$text = $this->getSerializedData();
1111
1112
			if ( $text !== null && $text !== false ) {
1113
				// Unserialize content
1114
				$handler = $this->getContentHandler();
1115
				$format = $this->getContentFormat();
1116
1117
				$this->mContent = $handler->unserializeContent( $text, $format );
0 ignored issues
show
Bug introduced by
It seems like $text defined by $this->getSerializedData() on line 1110 can also be of type boolean; however, ContentHandler::unserializeContent() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1118
			}
1119
		}
1120
1121
		// NOTE: copy() will return $this for immutable content objects
1122
		return $this->mContent ? $this->mContent->copy() : null;
1123
	}
1124
1125
	/**
1126
	 * Returns the content model for this revision.
1127
	 *
1128
	 * If no content model was stored in the database, the default content model for the title is
1129
	 * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
1130
	 * is used as a last resort.
1131
	 *
1132
	 * @return string The content model id associated with this revision,
1133
	 *     see the CONTENT_MODEL_XXX constants.
1134
	 **/
1135
	public function getContentModel() {
1136
		if ( !$this->mContentModel ) {
1137
			$title = $this->getTitle();
1138
			if ( $title ) {
1139
				$this->mContentModel = ContentHandler::getDefaultModelFor( $title );
1140
			} else {
1141
				$this->mContentModel = CONTENT_MODEL_WIKITEXT;
1142
			}
1143
1144
			assert( !empty( $this->mContentModel ) );
1145
		}
1146
1147
		return $this->mContentModel;
1148
	}
1149
1150
	/**
1151
	 * Returns the content format for this revision.
1152
	 *
1153
	 * If no content format was stored in the database, the default format for this
1154
	 * revision's content model is returned.
1155
	 *
1156
	 * @return string The content format id associated with this revision,
1157
	 *     see the CONTENT_FORMAT_XXX constants.
1158
	 **/
1159
	public function getContentFormat() {
1160
		if ( !$this->mContentFormat ) {
1161
			$handler = $this->getContentHandler();
1162
			$this->mContentFormat = $handler->getDefaultFormat();
1163
1164
			assert( !empty( $this->mContentFormat ) );
1165
		}
1166
1167
		return $this->mContentFormat;
1168
	}
1169
1170
	/**
1171
	 * Returns the content handler appropriate for this revision's content model.
1172
	 *
1173
	 * @throws MWException
1174
	 * @return ContentHandler
1175
	 */
1176
	public function getContentHandler() {
1177
		if ( !$this->mContentHandler ) {
1178
			$model = $this->getContentModel();
1179
			$this->mContentHandler = ContentHandler::getForModelID( $model );
1180
1181
			$format = $this->getContentFormat();
1182
1183
			if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
1184
				throw new MWException( "Oops, the content format $format is not supported for "
1185
					. "this content model, $model" );
1186
			}
1187
		}
1188
1189
		return $this->mContentHandler;
1190
	}
1191
1192
	/**
1193
	 * @return string
1194
	 */
1195
	public function getTimestamp() {
1196
		return wfTimestamp( TS_MW, $this->mTimestamp );
1197
	}
1198
1199
	/**
1200
	 * @return bool
1201
	 */
1202
	public function isCurrent() {
1203
		return $this->mCurrent;
1204
	}
1205
1206
	/**
1207
	 * Get previous revision for this title
1208
	 *
1209
	 * @return Revision|null
1210
	 */
1211 View Code Duplication
	public function getPrevious() {
1212
		if ( $this->getTitle() ) {
1213
			$prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
1214
			if ( $prev ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prev of type false|integer is loosely compared to true; 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...
1215
				return self::newFromTitle( $this->getTitle(), $prev );
1216
			}
1217
		}
1218
		return null;
1219
	}
1220
1221
	/**
1222
	 * Get next revision for this title
1223
	 *
1224
	 * @return Revision|null
1225
	 */
1226 View Code Duplication
	public function getNext() {
1227
		if ( $this->getTitle() ) {
1228
			$next = $this->getTitle()->getNextRevisionID( $this->getId() );
1229
			if ( $next ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $next of type false|integer is loosely compared to true; 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...
1230
				return self::newFromTitle( $this->getTitle(), $next );
1231
			}
1232
		}
1233
		return null;
1234
	}
1235
1236
	/**
1237
	 * Get previous revision Id for this page_id
1238
	 * This is used to populate rev_parent_id on save
1239
	 *
1240
	 * @param IDatabase $db
1241
	 * @return int
1242
	 */
1243
	private function getPreviousRevisionId( $db ) {
1244
		if ( $this->mPage === null ) {
1245
			return 0;
1246
		}
1247
		# Use page_latest if ID is not given
1248
		if ( !$this->mId ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mId 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...
1249
			$prevId = $db->selectField( 'page', 'page_latest',
1250
				[ 'page_id' => $this->mPage ],
1251
				__METHOD__ );
1252
		} else {
1253
			$prevId = $db->selectField( 'revision', 'rev_id',
1254
				[ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
1255
				__METHOD__,
1256
				[ 'ORDER BY' => 'rev_id DESC' ] );
1257
		}
1258
		return intval( $prevId );
1259
	}
1260
1261
	/**
1262
	 * Get revision text associated with an old or archive row
1263
	 * $row is usually an object from wfFetchRow(), both the flags and the text
1264
	 * field must be included.
1265
	 *
1266
	 * @param stdClass $row The text data
1267
	 * @param string $prefix Table prefix (default 'old_')
1268
	 * @param string|bool $wiki The name of the wiki to load the revision text from
1269
	 *   (same as the the wiki $row was loaded from) or false to indicate the local
1270
	 *   wiki (this is the default). Otherwise, it must be a symbolic wiki database
1271
	 *   identifier as understood by the LoadBalancer class.
1272
	 * @return string Text the text requested or false on failure
1273
	 */
1274
	public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
1275
1276
		# Get data
1277
		$textField = $prefix . 'text';
1278
		$flagsField = $prefix . 'flags';
1279
1280
		if ( isset( $row->$flagsField ) ) {
1281
			$flags = explode( ',', $row->$flagsField );
1282
		} else {
1283
			$flags = [];
1284
		}
1285
1286
		if ( isset( $row->$textField ) ) {
1287
			$text = $row->$textField;
1288
		} else {
1289
			return false;
1290
		}
1291
1292
		# Use external methods for external objects, text in table is URL-only then
1293
		if ( in_array( 'external', $flags ) ) {
1294
			$url = $text;
1295
			$parts = explode( '://', $url, 2 );
1296
			if ( count( $parts ) == 1 || $parts[1] == '' ) {
1297
				return false;
1298
			}
1299
			$text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
1300
		}
1301
1302
		// If the text was fetched without an error, convert it
1303
		if ( $text !== false ) {
1304
			$text = self::decompressRevisionText( $text, $flags );
1305
		}
1306
		return $text;
1307
	}
1308
1309
	/**
1310
	 * If $wgCompressRevisions is enabled, we will compress data.
1311
	 * The input string is modified in place.
1312
	 * Return value is the flags field: contains 'gzip' if the
1313
	 * data is compressed, and 'utf-8' if we're saving in UTF-8
1314
	 * mode.
1315
	 *
1316
	 * @param mixed $text Reference to a text
1317
	 * @return string
1318
	 */
1319
	public static function compressRevisionText( &$text ) {
1320
		global $wgCompressRevisions;
1321
		$flags = [];
1322
1323
		# Revisions not marked this way will be converted
1324
		# on load if $wgLegacyCharset is set in the future.
1325
		$flags[] = 'utf-8';
1326
1327
		if ( $wgCompressRevisions ) {
1328
			if ( function_exists( 'gzdeflate' ) ) {
1329
				$deflated = gzdeflate( $text );
1330
1331
				if ( $deflated === false ) {
1332
					wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
1333
				} else {
1334
					$text = $deflated;
1335
					$flags[] = 'gzip';
1336
				}
1337
			} else {
1338
				wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
1339
			}
1340
		}
1341
		return implode( ',', $flags );
1342
	}
1343
1344
	/**
1345
	 * Re-converts revision text according to it's flags.
1346
	 *
1347
	 * @param mixed $text Reference to a text
1348
	 * @param array $flags Compression flags
1349
	 * @return string|bool Decompressed text, or false on failure
1350
	 */
1351
	public static function decompressRevisionText( $text, $flags ) {
1352
		if ( in_array( 'gzip', $flags ) ) {
1353
			# Deal with optional compression of archived pages.
1354
			# This can be done periodically via maintenance/compressOld.php, and
1355
			# as pages are saved if $wgCompressRevisions is set.
1356
			$text = gzinflate( $text );
1357
1358
			if ( $text === false ) {
1359
				wfLogWarning( __METHOD__ . ': gzinflate() failed' );
1360
				return false;
1361
			}
1362
		}
1363
1364
		if ( in_array( 'object', $flags ) ) {
1365
			# Generic compressed storage
1366
			$obj = unserialize( $text );
1367
			if ( !is_object( $obj ) ) {
1368
				// Invalid object
1369
				return false;
1370
			}
1371
			$text = $obj->getText();
1372
		}
1373
1374
		global $wgLegacyEncoding;
1375
		if ( $text !== false && $wgLegacyEncoding
1376
			&& !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
1377
		) {
1378
			# Old revisions kept around in a legacy encoding?
1379
			# Upconvert on demand.
1380
			# ("utf8" checked for compatibility with some broken
1381
			#  conversion scripts 2008-12-30)
1382
			global $wgContLang;
1383
			$text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
1384
		}
1385
1386
		return $text;
1387
	}
1388
1389
	/**
1390
	 * Insert a new revision into the database, returning the new revision ID
1391
	 * number on success and dies horribly on failure.
1392
	 *
1393
	 * @param IDatabase $dbw (master connection)
1394
	 * @throws MWException
1395
	 * @return int
1396
	 */
1397
	public function insertOn( $dbw ) {
1398
		global $wgDefaultExternalStore, $wgContentHandlerUseDB;
1399
1400
		// We're inserting a new revision, so we have to use master anyway.
1401
		// If it's a null revision, it may have references to rows that
1402
		// are not in the replica yet (the text row).
1403
		$this->mQueryFlags |= self::READ_LATEST;
1404
1405
		// Not allowed to have rev_page equal to 0, false, etc.
1406 View Code Duplication
		if ( !$this->mPage ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mPage 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...
1407
			$title = $this->getTitle();
1408
			if ( $title instanceof Title ) {
1409
				$titleText = ' for page ' . $title->getPrefixedText();
1410
			} else {
1411
				$titleText = '';
1412
			}
1413
			throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
1414
		}
1415
1416
		$this->checkContentModel();
1417
1418
		$data = $this->mText;
1419
		$flags = self::compressRevisionText( $data );
1420
1421
		# Write to external storage if required
1422
		if ( $wgDefaultExternalStore ) {
1423
			// Store and get the URL
1424
			$data = ExternalStore::insertToDefault( $data );
1425
			if ( !$data ) {
1426
				throw new MWException( "Unable to store text to external storage" );
1427
			}
1428
			if ( $flags ) {
1429
				$flags .= ',';
1430
			}
1431
			$flags .= 'external';
1432
		}
1433
1434
		# Record the text (or external storage URL) to the text table
1435
		if ( $this->mTextId === null ) {
1436
			$old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
1437
			$dbw->insert( 'text',
1438
				[
1439
					'old_id' => $old_id,
1440
					'old_text' => $data,
1441
					'old_flags' => $flags,
1442
				], __METHOD__
1443
			);
1444
			$this->mTextId = $dbw->insertId();
1445
		}
1446
1447
		if ( $this->mComment === null ) {
1448
			$this->mComment = "";
1449
		}
1450
1451
		# Record the edit in revisions
1452
		$rev_id = $this->mId !== null
1453
			? $this->mId
1454
			: $dbw->nextSequenceValue( 'revision_rev_id_seq' );
1455
		$row = [
1456
			'rev_id'         => $rev_id,
1457
			'rev_page'       => $this->mPage,
1458
			'rev_text_id'    => $this->mTextId,
1459
			'rev_comment'    => $this->mComment,
1460
			'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
1461
			'rev_user'       => $this->mUser,
1462
			'rev_user_text'  => $this->mUserText,
1463
			'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
1464
			'rev_deleted'    => $this->mDeleted,
1465
			'rev_len'        => $this->mSize,
1466
			'rev_parent_id'  => $this->mParentId === null
1467
				? $this->getPreviousRevisionId( $dbw )
1468
				: $this->mParentId,
1469
			'rev_sha1'       => $this->mSha1 === null
1470
				? Revision::base36Sha1( $this->mText )
1471
				: $this->mSha1,
1472
		];
1473
1474
		if ( $wgContentHandlerUseDB ) {
1475
			// NOTE: Store null for the default model and format, to save space.
1476
			// XXX: Makes the DB sensitive to changed defaults.
1477
			// Make this behavior optional? Only in miser mode?
1478
1479
			$model = $this->getContentModel();
1480
			$format = $this->getContentFormat();
1481
1482
			$title = $this->getTitle();
1483
1484
			if ( $title === null ) {
1485
				throw new MWException( "Insufficient information to determine the title of the "
1486
					. "revision's page!" );
1487
			}
1488
1489
			$defaultModel = ContentHandler::getDefaultModelFor( $title );
1490
			$defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
1491
1492
			$row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
1493
			$row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
1494
		}
1495
1496
		$dbw->insert( 'revision', $row, __METHOD__ );
1497
1498
		$this->mId = $rev_id !== null ? $rev_id : $dbw->insertId();
1499
1500
		// Assertion to try to catch T92046
1501
		if ( (int)$this->mId === 0 ) {
1502
			throw new UnexpectedValueException(
1503
				'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
1504
					var_export( $row, 1 )
1505
			);
1506
		}
1507
1508
		Hooks::run( 'RevisionInsertComplete', [ &$this, $data, $flags ] );
1509
1510
		return $this->mId;
1511
	}
1512
1513
	protected function checkContentModel() {
1514
		global $wgContentHandlerUseDB;
1515
1516
		// Note: may return null for revisions that have not yet been inserted
1517
		$title = $this->getTitle();
1518
1519
		$model = $this->getContentModel();
1520
		$format = $this->getContentFormat();
1521
		$handler = $this->getContentHandler();
1522
1523
		if ( !$handler->isSupportedFormat( $format ) ) {
1524
			$t = $title->getPrefixedDBkey();
1525
1526
			throw new MWException( "Can't use format $format with content model $model on $t" );
1527
		}
1528
1529
		if ( !$wgContentHandlerUseDB && $title ) {
1530
			// if $wgContentHandlerUseDB is not set,
1531
			// all revisions must use the default content model and format.
1532
1533
			$defaultModel = ContentHandler::getDefaultModelFor( $title );
1534
			$defaultHandler = ContentHandler::getForModelID( $defaultModel );
1535
			$defaultFormat = $defaultHandler->getDefaultFormat();
1536
1537
			if ( $this->getContentModel() != $defaultModel ) {
1538
				$t = $title->getPrefixedDBkey();
1539
1540
				throw new MWException( "Can't save non-default content model with "
1541
					. "\$wgContentHandlerUseDB disabled: model is $model, "
1542
					. "default for $t is $defaultModel" );
1543
			}
1544
1545
			if ( $this->getContentFormat() != $defaultFormat ) {
1546
				$t = $title->getPrefixedDBkey();
1547
1548
				throw new MWException( "Can't use non-default content format with "
1549
					. "\$wgContentHandlerUseDB disabled: format is $format, "
1550
					. "default for $t is $defaultFormat" );
1551
			}
1552
		}
1553
1554
		$content = $this->getContent( Revision::RAW );
1555
		$prefixedDBkey = $title->getPrefixedDBkey();
1556
		$revId = $this->mId;
1557
1558
		if ( !$content ) {
1559
			throw new MWException(
1560
				"Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
1561
			);
1562
		}
1563
		if ( !$content->isValid() ) {
1564
			throw new MWException(
1565
				"Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
1566
			);
1567
		}
1568
	}
1569
1570
	/**
1571
	 * Get the base 36 SHA-1 value for a string of text
1572
	 * @param string $text
1573
	 * @return string
1574
	 */
1575
	public static function base36Sha1( $text ) {
1576
		return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
1577
	}
1578
1579
	/**
1580
	 * Lazy-load the revision's text.
1581
	 * Currently hardcoded to the 'text' table storage engine.
1582
	 *
1583
	 * @return string|bool The revision's text, or false on failure
1584
	 */
1585
	private function loadText() {
1586
		global $wgRevisionCacheExpiry;
1587
1588
		$cache = ObjectCache::getMainWANInstance();
1589
		if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
1590
			// Do not cache RDBMs blobs in...the RDBMs store
1591
			$ttl = $cache::TTL_UNCACHEABLE;
1592
		} else {
1593
			$ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
1594
		}
1595
1596
		// No negative caching; negative hits on text rows may be due to corrupted replica DBs
1597
		return $cache->getWithSetCallback(
1598
			$cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
1599
			$ttl,
1600
			function () {
1601
				return $this->fetchText();
1602
			},
1603
			[ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
1604
		);
1605
	}
1606
1607
	private function fetchText() {
1608
		$textId = $this->getTextId();
1609
1610
		// If we kept data for lazy extraction, use it now...
1611
		if ( $this->mTextRow !== null ) {
1612
			$row = $this->mTextRow;
1613
			$this->mTextRow = null;
1614
		} else {
1615
			$row = null;
1616
		}
1617
1618
		// Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
1619
		// do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
1620
		$flags = $this->mQueryFlags;
1621
		$flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
1622
			? self::READ_LATEST_IMMUTABLE
1623
			: 0;
1624
1625
		list( $index, $options, $fallbackIndex, $fallbackOptions ) =
1626
			DBAccessObjectUtils::getDBOptions( $flags );
1627
1628 View Code Duplication
		if ( !$row ) {
1629
			// Text data is immutable; check replica DBs first.
1630
			$row = wfGetDB( $index )->selectRow(
1631
				'text',
1632
				[ 'old_text', 'old_flags' ],
1633
				[ 'old_id' => $textId ],
1634
				__METHOD__,
1635
				$options
1636
			);
1637
		}
1638
1639
		// Fallback to DB_MASTER in some cases if the row was not found
1640 View Code Duplication
		if ( !$row && $fallbackIndex !== null ) {
1641
			// Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
1642
			// due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
1643
			$row = wfGetDB( $fallbackIndex )->selectRow(
1644
				'text',
1645
				[ 'old_text', 'old_flags' ],
1646
				[ 'old_id' => $textId ],
1647
				__METHOD__,
1648
				$fallbackOptions
1649
			);
1650
		}
1651
1652
		if ( !$row ) {
1653
			wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
1654
		}
1655
1656
		$text = self::getRevisionText( $row );
1657
		if ( $row && $text === false ) {
1658
			wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
1659
		}
1660
1661
		return is_string( $text ) ? $text : false;
1662
	}
1663
1664
	/**
1665
	 * Create a new null-revision for insertion into a page's
1666
	 * history. This will not re-save the text, but simply refer
1667
	 * to the text from the previous version.
1668
	 *
1669
	 * Such revisions can for instance identify page rename
1670
	 * operations and other such meta-modifications.
1671
	 *
1672
	 * @param IDatabase $dbw
1673
	 * @param int $pageId ID number of the page to read from
1674
	 * @param string $summary Revision's summary
1675
	 * @param bool $minor Whether the revision should be considered as minor
1676
	 * @param User|null $user User object to use or null for $wgUser
1677
	 * @return Revision|null Revision or null on error
1678
	 */
1679
	public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
1680
		global $wgContentHandlerUseDB, $wgContLang;
1681
1682
		$fields = [ 'page_latest', 'page_namespace', 'page_title',
1683
						'rev_text_id', 'rev_len', 'rev_sha1' ];
1684
1685
		if ( $wgContentHandlerUseDB ) {
1686
			$fields[] = 'rev_content_model';
1687
			$fields[] = 'rev_content_format';
1688
		}
1689
1690
		$current = $dbw->selectRow(
1691
			[ 'page', 'revision' ],
1692
			$fields,
1693
			[
1694
				'page_id' => $pageId,
1695
				'page_latest=rev_id',
1696
			],
1697
			__METHOD__,
1698
			[ 'FOR UPDATE' ] // T51581
1699
		);
1700
1701
		if ( $current ) {
1702
			if ( !$user ) {
1703
				global $wgUser;
1704
				$user = $wgUser;
1705
			}
1706
1707
			// Truncate for whole multibyte characters
1708
			$summary = $wgContLang->truncate( $summary, 255 );
1709
1710
			$row = [
1711
				'page'       => $pageId,
1712
				'user_text'  => $user->getName(),
1713
				'user'       => $user->getId(),
1714
				'comment'    => $summary,
1715
				'minor_edit' => $minor,
1716
				'text_id'    => $current->rev_text_id,
1717
				'parent_id'  => $current->page_latest,
1718
				'len'        => $current->rev_len,
1719
				'sha1'       => $current->rev_sha1
1720
			];
1721
1722
			if ( $wgContentHandlerUseDB ) {
1723
				$row['content_model'] = $current->rev_content_model;
1724
				$row['content_format'] = $current->rev_content_format;
1725
			}
1726
1727
			$row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
1728
1729
			$revision = new Revision( $row );
1730
		} else {
1731
			$revision = null;
1732
		}
1733
1734
		return $revision;
1735
	}
1736
1737
	/**
1738
	 * Determine if the current user is allowed to view a particular
1739
	 * field of this revision, if it's marked as deleted.
1740
	 *
1741
	 * @param int $field One of self::DELETED_TEXT,
1742
	 *                              self::DELETED_COMMENT,
1743
	 *                              self::DELETED_USER
1744
	 * @param User|null $user User object to check, or null to use $wgUser
1745
	 * @return bool
1746
	 */
1747
	public function userCan( $field, User $user = null ) {
1748
		return self::userCanBitfield( $this->getVisibility(), $field, $user );
1749
	}
1750
1751
	/**
1752
	 * Determine if the current user is allowed to view a particular
1753
	 * field of this revision, if it's marked as deleted. This is used
1754
	 * by various classes to avoid duplication.
1755
	 *
1756
	 * @param int $bitfield Current field
1757
	 * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
1758
	 *                               self::DELETED_COMMENT = File::DELETED_COMMENT,
1759
	 *                               self::DELETED_USER = File::DELETED_USER
1760
	 * @param User|null $user User object to check, or null to use $wgUser
1761
	 * @param Title|null $title A Title object to check for per-page restrictions on,
1762
	 *                          instead of just plain userrights
1763
	 * @return bool
1764
	 */
1765
	public static function userCanBitfield( $bitfield, $field, User $user = null,
1766
		Title $title = null
1767
	) {
1768
		if ( $bitfield & $field ) { // aspect is deleted
1769
			if ( $user === null ) {
1770
				global $wgUser;
1771
				$user = $wgUser;
1772
			}
1773
			if ( $bitfield & self::DELETED_RESTRICTED ) {
1774
				$permissions = [ 'suppressrevision', 'viewsuppressed' ];
1775
			} elseif ( $field & self::DELETED_TEXT ) {
1776
				$permissions = [ 'deletedtext' ];
1777
			} else {
1778
				$permissions = [ 'deletedhistory' ];
1779
			}
1780
			$permissionlist = implode( ', ', $permissions );
1781
			if ( $title === null ) {
1782
				wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
1783
				return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
1784
			} else {
1785
				$text = $title->getPrefixedText();
1786
				wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
1787
				foreach ( $permissions as $perm ) {
1788
					if ( $title->userCan( $perm, $user ) ) {
1789
						return true;
1790
					}
1791
				}
1792
				return false;
1793
			}
1794
		} else {
1795
			return true;
1796
		}
1797
	}
1798
1799
	/**
1800
	 * Get rev_timestamp from rev_id, without loading the rest of the row
1801
	 *
1802
	 * @param Title $title
1803
	 * @param int $id
1804
	 * @return string|bool False if not found
1805
	 */
1806
	static function getTimestampFromId( $title, $id, $flags = 0 ) {
1807
		$db = ( $flags & self::READ_LATEST )
1808
			? wfGetDB( DB_MASTER )
1809
			: wfGetDB( DB_REPLICA );
1810
		// Casting fix for databases that can't take '' for rev_id
1811
		if ( $id == '' ) {
1812
			$id = 0;
1813
		}
1814
		$conds = [ 'rev_id' => $id ];
1815
		$conds['rev_page'] = $title->getArticleID();
1816
		$timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
1817
1818
		return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
1819
	}
1820
1821
	/**
1822
	 * Get count of revisions per page...not very efficient
1823
	 *
1824
	 * @param IDatabase $db
1825
	 * @param int $id Page id
1826
	 * @return int
1827
	 */
1828
	static function countByPageId( $db, $id ) {
1829
		$row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
1830
			[ 'rev_page' => $id ], __METHOD__ );
1831
		if ( $row ) {
1832
			return $row->revCount;
1833
		}
1834
		return 0;
1835
	}
1836
1837
	/**
1838
	 * Get count of revisions per page...not very efficient
1839
	 *
1840
	 * @param IDatabase $db
1841
	 * @param Title $title
1842
	 * @return int
1843
	 */
1844
	static function countByTitle( $db, $title ) {
1845
		$id = $title->getArticleID();
1846
		if ( $id ) {
1847
			return self::countByPageId( $db, $id );
1848
		}
1849
		return 0;
1850
	}
1851
1852
	/**
1853
	 * Check if no edits were made by other users since
1854
	 * the time a user started editing the page. Limit to
1855
	 * 50 revisions for the sake of performance.
1856
	 *
1857
	 * @since 1.20
1858
	 * @deprecated since 1.24
1859
	 *
1860
	 * @param IDatabase|int $db The Database to perform the check on. May be given as a
1861
	 *        Database object or a database identifier usable with wfGetDB.
1862
	 * @param int $pageId The ID of the page in question
1863
	 * @param int $userId The ID of the user in question
1864
	 * @param string $since Look at edits since this time
1865
	 *
1866
	 * @return bool True if the given user was the only one to edit since the given timestamp
1867
	 */
1868
	public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
1869
		if ( !$userId ) {
1870
			return false;
1871
		}
1872
1873
		if ( is_int( $db ) ) {
1874
			$db = wfGetDB( $db );
1875
		}
1876
1877
		$res = $db->select( 'revision',
1878
			'rev_user',
1879
			[
1880
				'rev_page' => $pageId,
1881
				'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
0 ignored issues
show
Bug introduced by
It seems like $db is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1882
			],
1883
			__METHOD__,
1884
			[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
1885
		foreach ( $res as $row ) {
1886
			if ( $row->rev_user != $userId ) {
1887
				return false;
1888
			}
1889
		}
1890
		return true;
1891
	}
1892
1893
	/**
1894
	 * Load a revision based on a known page ID and current revision ID from the DB
1895
	 *
1896
	 * This method allows for the use of caching, though accessing anything that normally
1897
	 * requires permission checks (aside from the text) will trigger a small DB lookup.
1898
	 * The title will also be lazy loaded, though setTitle() can be used to preload it.
1899
	 *
1900
	 * @param IDatabase $db
1901
	 * @param int $pageId Page ID
1902
	 * @param int $revId Known current revision of this page
1903
	 * @return Revision|bool Returns false if missing
1904
	 * @since 1.28
1905
	 */
1906
	public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
1907
		$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1908
		return $cache->getWithSetCallback(
1909
			// Page/rev IDs passed in from DB to reflect history merges
1910
			$cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ),
1911
			$cache::TTL_WEEK,
1912
			function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
1913
				$setOpts += Database::getCacheSetOptions( $db );
1914
1915
				$rev = Revision::loadFromPageId( $db, $pageId, $revId );
1916
				// Reflect revision deletion and user renames
1917
				if ( $rev ) {
1918
					$rev->mTitle = null; // mutable; lazy-load
1919
					$rev->mRefreshMutableFields = true;
1920
				}
1921
1922
				return $rev ?: false; // don't cache negatives
1923
			}
1924
		);
1925
	}
1926
1927
	/**
1928
	 * For cached revisions, make sure the user name and rev_deleted is up-to-date
1929
	 */
1930
	private function loadMutableFields() {
1931
		if ( !$this->mRefreshMutableFields ) {
1932
			return; // not needed
1933
		}
1934
1935
		$this->mRefreshMutableFields = false;
1936
		$dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

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

Loading history...
1937
		$row = $dbr->selectRow(
1938
			[ 'revision', 'user' ],
1939
			[ 'rev_deleted', 'user_name' ],
1940
			[ 'rev_id' => $this->mId, 'user_id = rev_user' ],
1941
			__METHOD__
1942
		);
1943
		if ( $row ) { // update values
1944
			$this->mDeleted = (int)$row->rev_deleted;
1945
			$this->mUserText = $row->user_name;
1946
		}
1947
	}
1948
}
1949