Completed
Branch master (7e350b)
by
unknown
30:36
created

Revision::loadMutableFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
nc 3
nop 0
dl 0
loc 18
rs 9.4285
c 1
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
24
/**
25
 * @todo document
26
 */
27
class Revision implements IDBAccessObject {
28
	/** @var int|null */
29
	protected $mId;
30
	/** @var int|null */
31
	protected $mPage;
32
	/** @var string */
33
	protected $mUserText;
34
	/** @var string */
35
	protected $mOrigUserText;
36
	/** @var int */
37
	protected $mUser;
38
	/** @var bool */
39
	protected $mMinorEdit;
40
	/** @var string */
41
	protected $mTimestamp;
42
	/** @var int */
43
	protected $mDeleted;
44
	/** @var int */
45
	protected $mSize;
46
	/** @var string */
47
	protected $mSha1;
48
	/** @var int */
49
	protected $mParentId;
50
	/** @var string */
51
	protected $mComment;
52
	/** @var string */
53
	protected $mText;
54
	/** @var int */
55
	protected $mTextId;
56
	/** @var int */
57
	protected $mUnpatrolled;
58
59
	/** @var stdClass|null */
60
	protected $mTextRow;
61
62
	/**  @var null|Title */
63
	protected $mTitle;
64
	/** @var bool */
65
	protected $mCurrent;
66
	/** @var string */
67
	protected $mContentModel;
68
	/** @var string */
69
	protected $mContentFormat;
70
71
	/** @var Content|null|bool */
72
	protected $mContent;
73
	/** @var null|ContentHandler */
74
	protected $mContentHandler;
75
76
	/** @var int */
77
	protected $mQueryFlags = 0;
78
	/** @var bool Used for cached values to reload user text and rev_deleted */
79
	protected $mRefreshMutableFields = false;
80
	/** @var string Wiki ID; false means the current wiki */
81
	protected $mWiki = false;
82
83
	// Revision deletion constants
84
	const DELETED_TEXT = 1;
85
	const DELETED_COMMENT = 2;
86
	const DELETED_USER = 4;
87
	const DELETED_RESTRICTED = 8;
88
	const SUPPRESSED_USER = 12; // convenience
89
90
	// Audience options for accessors
91
	const FOR_PUBLIC = 1;
92
	const FOR_THIS_USER = 2;
93
	const RAW = 3;
94
95
	/**
96
	 * Load a page revision from a given revision ID number.
97
	 * Returns null if no such revision can be found.
98
	 *
99
	 * $flags include:
100
	 *      Revision::READ_LATEST  : Select the data from the master
101
	 *      Revision::READ_LOCKING : Select & lock the data from the master
102
	 *
103
	 * @param int $id
104
	 * @param int $flags (optional)
105
	 * @return Revision|null
106
	 */
107
	public static function newFromId( $id, $flags = 0 ) {
108
		return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
109
	}
110
111
	/**
112
	 * Load either the current, or a specified, revision
113
	 * that's attached to a given link target. If not attached
114
	 * to that link target, will return null.
115
	 *
116
	 * $flags include:
117
	 *      Revision::READ_LATEST  : Select the data from the master
118
	 *      Revision::READ_LOCKING : Select & lock the data from the master
119
	 *
120
	 * @param LinkTarget $linkTarget
121
	 * @param int $id (optional)
122
	 * @param int $flags Bitfield (optional)
123
	 * @return Revision|null
124
	 */
125 View Code Duplication
	public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
126
		$conds = [
127
			'page_namespace' => $linkTarget->getNamespace(),
128
			'page_title' => $linkTarget->getDBkey()
129
		];
130
		if ( $id ) {
131
			// Use the specified ID
132
			$conds['rev_id'] = $id;
133
			return self::newFromConds( $conds, $flags );
134
		} else {
135
			// Use a join to get the latest revision
136
			$conds[] = 'rev_id=page_latest';
137
			$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
138
			return self::loadFromConds( $db, $conds, $flags );
139
		}
140
	}
141
142
	/**
143
	 * Load either the current, or a specified, revision
144
	 * that's attached to a given page ID.
145
	 * Returns null if no such revision can be found.
146
	 *
147
	 * $flags include:
148
	 *      Revision::READ_LATEST  : Select the data from the master (since 1.20)
149
	 *      Revision::READ_LOCKING : Select & lock the data from the master
150
	 *
151
	 * @param int $pageId
152
	 * @param int $revId (optional)
153
	 * @param int $flags Bitfield (optional)
154
	 * @return Revision|null
155
	 */
156 View Code Duplication
	public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
157
		$conds = [ 'page_id' => $pageId ];
158
		if ( $revId ) {
159
			$conds['rev_id'] = $revId;
160
			return self::newFromConds( $conds, $flags );
161
		} else {
162
			// Use a join to get the latest revision
163
			$conds[] = 'rev_id = page_latest';
164
			$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
165
			return self::loadFromConds( $db, $conds, $flags );
166
		}
167
	}
168
169
	/**
170
	 * Make a fake revision object from an archive table row. This is queried
171
	 * for permissions or even inserted (as in Special:Undelete)
172
	 * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
173
	 *
174
	 * @param object $row
175
	 * @param array $overrides
176
	 *
177
	 * @throws MWException
178
	 * @return Revision
179
	 */
180
	public static function newFromArchiveRow( $row, $overrides = [] ) {
181
		global $wgContentHandlerUseDB;
182
183
		$attribs = $overrides + [
184
			'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
185
			'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
186
			'comment'    => $row->ar_comment,
187
			'user'       => $row->ar_user,
188
			'user_text'  => $row->ar_user_text,
189
			'timestamp'  => $row->ar_timestamp,
190
			'minor_edit' => $row->ar_minor_edit,
191
			'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
192
			'deleted'    => $row->ar_deleted,
193
			'len'        => $row->ar_len,
194
			'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
195
			'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
196
			'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
197
		];
198
199
		if ( !$wgContentHandlerUseDB ) {
200
			unset( $attribs['content_model'] );
201
			unset( $attribs['content_format'] );
202
		}
203
204
		if ( !isset( $attribs['title'] )
205
			&& isset( $row->ar_namespace )
206
			&& isset( $row->ar_title )
207
		) {
208
			$attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
209
		}
210
211
		if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
212
			// Pre-1.5 ar_text row
213
			$attribs['text'] = self::getRevisionText( $row, 'ar_' );
214
			if ( $attribs['text'] === false ) {
215
				throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
216
			}
217
		}
218
		return new self( $attribs );
219
	}
220
221
	/**
222
	 * @since 1.19
223
	 *
224
	 * @param object $row
225
	 * @return Revision
226
	 */
227
	public static function newFromRow( $row ) {
228
		return new self( $row );
229
	}
230
231
	/**
232
	 * Load a page revision from a given revision ID number.
233
	 * Returns null if no such revision can be found.
234
	 *
235
	 * @param IDatabase $db
236
	 * @param int $id
237
	 * @return Revision|null
238
	 */
239
	public static function loadFromId( $db, $id ) {
240
		return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
241
	}
242
243
	/**
244
	 * Load either the current, or a specified, revision
245
	 * that's attached to a given page. If not attached
246
	 * to that page, will return null.
247
	 *
248
	 * @param IDatabase $db
249
	 * @param int $pageid
250
	 * @param int $id
251
	 * @return Revision|null
252
	 */
253
	public static function loadFromPageId( $db, $pageid, $id = 0 ) {
254
		$conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
255
		if ( $id ) {
256
			$conds['rev_id'] = intval( $id );
257
		} else {
258
			$conds[] = 'rev_id=page_latest';
259
		}
260
		return self::loadFromConds( $db, $conds );
261
	}
262
263
	/**
264
	 * Load either the current, or a specified, revision
265
	 * that's attached to a given page. If not attached
266
	 * to that page, will return null.
267
	 *
268
	 * @param IDatabase $db
269
	 * @param Title $title
270
	 * @param int $id
271
	 * @return Revision|null
272
	 */
273
	public static function loadFromTitle( $db, $title, $id = 0 ) {
274
		if ( $id ) {
275
			$matchId = intval( $id );
276
		} else {
277
			$matchId = 'page_latest';
278
		}
279
		return self::loadFromConds( $db,
280
			[
281
				"rev_id=$matchId",
282
				'page_namespace' => $title->getNamespace(),
283
				'page_title' => $title->getDBkey()
284
			]
285
		);
286
	}
287
288
	/**
289
	 * Load the revision for the given title with the given timestamp.
290
	 * WARNING: Timestamps may in some circumstances not be unique,
291
	 * so this isn't the best key to use.
292
	 *
293
	 * @param IDatabase $db
294
	 * @param Title $title
295
	 * @param string $timestamp
296
	 * @return Revision|null
297
	 */
298
	public static function loadFromTimestamp( $db, $title, $timestamp ) {
299
		return self::loadFromConds( $db,
300
			[
301
				'rev_timestamp' => $db->timestamp( $timestamp ),
302
				'page_namespace' => $title->getNamespace(),
303
				'page_title' => $title->getDBkey()
304
			]
305
		);
306
	}
307
308
	/**
309
	 * Given a set of conditions, fetch a revision
310
	 *
311
	 * This method is used then a revision ID is qualified and
312
	 * will incorporate some basic replica DB/master fallback logic
313
	 *
314
	 * @param array $conditions
315
	 * @param int $flags (optional)
316
	 * @return Revision|null
317
	 */
318
	private static function newFromConds( $conditions, $flags = 0 ) {
319
		$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
320
321
		$rev = self::loadFromConds( $db, $conditions, $flags );
322
		// Make sure new pending/committed revision are visibile later on
323
		// within web requests to certain avoid bugs like T93866 and T94407.
324
		if ( !$rev
325
			&& !( $flags & self::READ_LATEST )
326
			&& 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...
327
			&& 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...
328
		) {
329
			$flags = self::READ_LATEST;
330
			$db = wfGetDB( DB_MASTER );
331
			$rev = self::loadFromConds( $db, $conditions, $flags );
332
		}
333
334
		if ( $rev ) {
335
			$rev->mQueryFlags = $flags;
336
		}
337
338
		return $rev;
339
	}
340
341
	/**
342
	 * Given a set of conditions, fetch a revision from
343
	 * the given database connection.
344
	 *
345
	 * @param IDatabase $db
346
	 * @param array $conditions
347
	 * @param int $flags (optional)
348
	 * @return Revision|null
349
	 */
350
	private static function loadFromConds( $db, $conditions, $flags = 0 ) {
351
		$row = self::fetchFromConds( $db, $conditions, $flags );
352
		if ( $row ) {
353
			$rev = new Revision( $row );
354
			$rev->mWiki = $db->getWikiID();
355
356
			return $rev;
357
		}
358
359
		return null;
360
	}
361
362
	/**
363
	 * Return a wrapper for a series of database rows to
364
	 * fetch all of a given page's revisions in turn.
365
	 * Each row can be fed to the constructor to get objects.
366
	 *
367
	 * @param LinkTarget $title
368
	 * @return ResultWrapper
369
	 * @deprecated Since 1.28
370
	 */
371
	public static function fetchRevision( LinkTarget $title ) {
372
		$row = self::fetchFromConds(
373
			wfGetDB( DB_REPLICA ),
374
			[
375
				'rev_id=page_latest',
376
				'page_namespace' => $title->getNamespace(),
377
				'page_title' => $title->getDBkey()
378
			]
379
		);
380
381
		return new FakeResultWrapper( $row ? [ $row ] : [] );
382
	}
383
384
	/**
385
	 * Given a set of conditions, return a ResultWrapper
386
	 * which will return matching database rows with the
387
	 * fields necessary to build Revision objects.
388
	 *
389
	 * @param IDatabase $db
390
	 * @param array $conditions
391
	 * @param int $flags (optional)
392
	 * @return stdClass
393
	 */
394
	private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
395
		$fields = array_merge(
396
			self::selectFields(),
397
			self::selectPageFields(),
398
			self::selectUserFields()
399
		);
400
		$options = [];
401
		if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
402
			$options[] = 'FOR UPDATE';
403
		}
404
		return $db->selectRow(
405
			[ 'revision', 'page', 'user' ],
406
			$fields,
407
			$conditions,
408
			__METHOD__,
409
			$options,
410
			[ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ]
411
		);
412
	}
413
414
	/**
415
	 * Return the value of a select() JOIN conds array for the user table.
416
	 * This will get user table rows for logged-in users.
417
	 * @since 1.19
418
	 * @return array
419
	 */
420
	public static function userJoinCond() {
421
		return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
422
	}
423
424
	/**
425
	 * Return the value of a select() page conds array for the page table.
426
	 * This will assure that the revision(s) are not orphaned from live pages.
427
	 * @since 1.19
428
	 * @return array
429
	 */
430
	public static function pageJoinCond() {
431
		return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
432
	}
433
434
	/**
435
	 * Return the list of revision fields that should be selected to create
436
	 * a new revision.
437
	 * @return array
438
	 */
439
	public static function selectFields() {
440
		global $wgContentHandlerUseDB;
441
442
		$fields = [
443
			'rev_id',
444
			'rev_page',
445
			'rev_text_id',
446
			'rev_timestamp',
447
			'rev_comment',
448
			'rev_user_text',
449
			'rev_user',
450
			'rev_minor_edit',
451
			'rev_deleted',
452
			'rev_len',
453
			'rev_parent_id',
454
			'rev_sha1',
455
		];
456
457
		if ( $wgContentHandlerUseDB ) {
458
			$fields[] = 'rev_content_format';
459
			$fields[] = 'rev_content_model';
460
		}
461
462
		return $fields;
463
	}
464
465
	/**
466
	 * Return the list of revision fields that should be selected to create
467
	 * a new revision from an archive row.
468
	 * @return array
469
	 */
470
	public static function selectArchiveFields() {
471
		global $wgContentHandlerUseDB;
472
		$fields = [
473
			'ar_id',
474
			'ar_page_id',
475
			'ar_rev_id',
476
			'ar_text',
477
			'ar_text_id',
478
			'ar_timestamp',
479
			'ar_comment',
480
			'ar_user_text',
481
			'ar_user',
482
			'ar_minor_edit',
483
			'ar_deleted',
484
			'ar_len',
485
			'ar_parent_id',
486
			'ar_sha1',
487
		];
488
489
		if ( $wgContentHandlerUseDB ) {
490
			$fields[] = 'ar_content_format';
491
			$fields[] = 'ar_content_model';
492
		}
493
		return $fields;
494
	}
495
496
	/**
497
	 * Return the list of text fields that should be selected to read the
498
	 * revision text
499
	 * @return array
500
	 */
501
	public static function selectTextFields() {
502
		return [
503
			'old_text',
504
			'old_flags'
505
		];
506
	}
507
508
	/**
509
	 * Return the list of page fields that should be selected from page table
510
	 * @return array
511
	 */
512
	public static function selectPageFields() {
513
		return [
514
			'page_namespace',
515
			'page_title',
516
			'page_id',
517
			'page_latest',
518
			'page_is_redirect',
519
			'page_len',
520
		];
521
	}
522
523
	/**
524
	 * Return the list of user fields that should be selected from user table
525
	 * @return array
526
	 */
527
	public static function selectUserFields() {
528
		return [ 'user_name' ];
529
	}
530
531
	/**
532
	 * Do a batched query to get the parent revision lengths
533
	 * @param IDatabase $db
534
	 * @param array $revIds
535
	 * @return array
536
	 */
537
	public static function getParentLengths( $db, array $revIds ) {
538
		$revLens = [];
539
		if ( !$revIds ) {
540
			return $revLens; // empty
541
		}
542
		$res = $db->select( 'revision',
543
			[ 'rev_id', 'rev_len' ],
544
			[ 'rev_id' => $revIds ],
545
			__METHOD__ );
546
		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...
547
			$revLens[$row->rev_id] = $row->rev_len;
548
		}
549
		return $revLens;
550
	}
551
552
	/**
553
	 * Constructor
554
	 *
555
	 * @param object|array $row Either a database row or an array
556
	 * @throws MWException
557
	 * @access private
558
	 */
559
	function __construct( $row ) {
560
		if ( is_object( $row ) ) {
561
			$this->mId = intval( $row->rev_id );
562
			$this->mPage = intval( $row->rev_page );
563
			$this->mTextId = intval( $row->rev_text_id );
564
			$this->mComment = $row->rev_comment;
565
			$this->mUser = intval( $row->rev_user );
566
			$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...
567
			$this->mTimestamp = $row->rev_timestamp;
568
			$this->mDeleted = intval( $row->rev_deleted );
569
570
			if ( !isset( $row->rev_parent_id ) ) {
571
				$this->mParentId = null;
572
			} else {
573
				$this->mParentId = intval( $row->rev_parent_id );
574
			}
575
576
			if ( !isset( $row->rev_len ) ) {
577
				$this->mSize = null;
578
			} else {
579
				$this->mSize = intval( $row->rev_len );
580
			}
581
582
			if ( !isset( $row->rev_sha1 ) ) {
583
				$this->mSha1 = null;
584
			} else {
585
				$this->mSha1 = $row->rev_sha1;
586
			}
587
588
			if ( isset( $row->page_latest ) ) {
589
				$this->mCurrent = ( $row->rev_id == $row->page_latest );
590
				$this->mTitle = Title::newFromRow( $row );
591
			} else {
592
				$this->mCurrent = false;
593
				$this->mTitle = null;
594
			}
595
596
			if ( !isset( $row->rev_content_model ) ) {
597
				$this->mContentModel = null; # determine on demand if needed
598
			} else {
599
				$this->mContentModel = strval( $row->rev_content_model );
600
			}
601
602
			if ( !isset( $row->rev_content_format ) ) {
603
				$this->mContentFormat = null; # determine on demand if needed
604
			} else {
605
				$this->mContentFormat = strval( $row->rev_content_format );
606
			}
607
608
			// Lazy extraction...
609
			$this->mText = null;
610
			if ( isset( $row->old_text ) ) {
611
				$this->mTextRow = $row;
612
			} else {
613
				// 'text' table row entry will be lazy-loaded
614
				$this->mTextRow = null;
615
			}
616
617
			// Use user_name for users and rev_user_text for IPs...
618
			$this->mUserText = null; // lazy load if left null
619
			if ( $this->mUser == 0 ) {
620
				$this->mUserText = $row->rev_user_text; // IP user
621
			} elseif ( isset( $row->user_name ) ) {
622
				$this->mUserText = $row->user_name; // logged-in user
623
			}
624
			$this->mOrigUserText = $row->rev_user_text;
625
		} elseif ( is_array( $row ) ) {
626
			// Build a new revision to be saved...
627
			global $wgUser; // ugh
628
629
			# if we have a content object, use it to set the model and type
630
			if ( !empty( $row['content'] ) ) {
631
				// @todo when is that set? test with external store setup! check out insertOn() [dk]
632
				if ( !empty( $row['text_id'] ) ) {
633
					throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
634
						"can't serialize content object" );
635
				}
636
637
				$row['content_model'] = $row['content']->getModel();
638
				# note: mContentFormat is initializes later accordingly
639
				# note: content is serialized later in this method!
640
				# also set text to null?
641
			}
642
643
			$this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
644
			$this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
645
			$this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
646
			$this->mUserText = isset( $row['user_text'] )
647
				? strval( $row['user_text'] ) : $wgUser->getName();
648
			$this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
649
			$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...
650
			$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...
651
				? strval( $row['timestamp'] ) : wfTimestampNow();
652
			$this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
653
			$this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
654
			$this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
655
			$this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
656
657
			$this->mContentModel = isset( $row['content_model'] )
658
				? strval( $row['content_model'] ) : null;
659
			$this->mContentFormat = isset( $row['content_format'] )
660
				? strval( $row['content_format'] ) : null;
661
662
			// Enforce spacing trimming on supplied text
663
			$this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
664
			$this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
665
			$this->mTextRow = null;
666
667
			$this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
668
669
			// if we have a Content object, override mText and mContentModel
670
			if ( !empty( $row['content'] ) ) {
671
				if ( !( $row['content'] instanceof Content ) ) {
672
					throw new MWException( '`content` field must contain a Content object.' );
673
				}
674
675
				$handler = $this->getContentHandler();
676
				$this->mContent = $row['content'];
677
678
				$this->mContentModel = $this->mContent->getModel();
679
				$this->mContentHandler = null;
680
681
				$this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
682
			} elseif ( $this->mText !== null ) {
683
				$handler = $this->getContentHandler();
684
				$this->mContent = $handler->unserializeContent( $this->mText );
685
			}
686
687
			// If we have a Title object, make sure it is consistent with mPage.
688
			if ( $this->mTitle && $this->mTitle->exists() ) {
689
				if ( $this->mPage === null ) {
690
					// if the page ID wasn't known, set it now
691
					$this->mPage = $this->mTitle->getArticleID();
692
				} elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
693
					// Got different page IDs. This may be legit (e.g. during undeletion),
694
					// but it seems worth mentioning it in the log.
695
					wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
696
						$this->mTitle->getArticleID() . " provided by the Title object." );
697
				}
698
			}
699
700
			$this->mCurrent = false;
701
702
			// If we still have no length, see it we have the text to figure it out
703
			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...
704
				$this->mSize = $this->mContent->getSize();
705
			}
706
707
			// Same for sha1
708
			if ( $this->mSha1 === null ) {
709
				$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...
710
			}
711
712
			// force lazy init
713
			$this->getContentModel();
714
			$this->getContentFormat();
715
		} else {
716
			throw new MWException( 'Revision constructor passed invalid row format.' );
717
		}
718
		$this->mUnpatrolled = null;
719
	}
720
721
	/**
722
	 * Get revision ID
723
	 *
724
	 * @return int|null
725
	 */
726
	public function getId() {
727
		return $this->mId;
728
	}
729
730
	/**
731
	 * Set the revision ID
732
	 *
733
	 * This should only be used for proposed revisions that turn out to be null edits
734
	 *
735
	 * @since 1.19
736
	 * @param int $id
737
	 */
738
	public function setId( $id ) {
739
		$this->mId = (int)$id;
740
	}
741
742
	/**
743
	 * Set the user ID/name
744
	 *
745
	 * This should only be used for proposed revisions that turn out to be null edits
746
	 *
747
	 * @since 1.28
748
	 * @param integer $id User ID
749
	 * @param string $name User name
750
	 */
751
	public function setUserIdAndName( $id, $name ) {
752
		$this->mUser = (int)$id;
753
		$this->mUserText = $name;
754
		$this->mOrigUserText = $name;
755
	}
756
757
	/**
758
	 * Get text row ID
759
	 *
760
	 * @return int|null
761
	 */
762
	public function getTextId() {
763
		return $this->mTextId;
764
	}
765
766
	/**
767
	 * Get parent revision ID (the original previous page revision)
768
	 *
769
	 * @return int|null
770
	 */
771
	public function getParentId() {
772
		return $this->mParentId;
773
	}
774
775
	/**
776
	 * Returns the length of the text in this revision, or null if unknown.
777
	 *
778
	 * @return int|null
779
	 */
780
	public function getSize() {
781
		return $this->mSize;
782
	}
783
784
	/**
785
	 * Returns the base36 sha1 of the text in this revision, or null if unknown.
786
	 *
787
	 * @return string|null
788
	 */
789
	public function getSha1() {
790
		return $this->mSha1;
791
	}
792
793
	/**
794
	 * Returns the title of the page associated with this entry or null.
795
	 *
796
	 * Will do a query, when title is not set and id is given.
797
	 *
798
	 * @return Title|null
799
	 */
800
	public function getTitle() {
801
		if ( $this->mTitle !== null ) {
802
			return $this->mTitle;
803
		}
804
		// rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
805
		if ( $this->mId !== null ) {
806
			$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...
807
			$row = $dbr->selectRow(
808
				[ 'page', 'revision' ],
809
				self::selectPageFields(),
810
				[ 'page_id=rev_page', 'rev_id' => $this->mId ],
811
				__METHOD__
812
			);
813
			if ( $row ) {
814
				// @TODO: better foreign title handling
815
				$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 807 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...
816
			}
817
		}
818
819
		if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
820
			// Loading by ID is best, though not possible for foreign titles
821
			if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
822
				$this->mTitle = Title::newFromID( $this->mPage );
823
			}
824
		}
825
826
		return $this->mTitle;
827
	}
828
829
	/**
830
	 * Set the title of the revision
831
	 *
832
	 * @param Title $title
833
	 */
834
	public function setTitle( $title ) {
835
		$this->mTitle = $title;
836
	}
837
838
	/**
839
	 * Get the page ID
840
	 *
841
	 * @return int|null
842
	 */
843
	public function getPage() {
844
		return $this->mPage;
845
	}
846
847
	/**
848
	 * Fetch revision's user id if it's available to the specified audience.
849
	 * If the specified audience does not have access to it, zero will be
850
	 * returned.
851
	 *
852
	 * @param int $audience One of:
853
	 *   Revision::FOR_PUBLIC       to be displayed to all users
854
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
855
	 *   Revision::RAW              get the ID regardless of permissions
856
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
857
	 *   to the $audience parameter
858
	 * @return int
859
	 */
860 View Code Duplication
	public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
861
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
862
			return 0;
863
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
864
			return 0;
865
		} else {
866
			return $this->mUser;
867
		}
868
	}
869
870
	/**
871
	 * Fetch revision's user id without regard for the current user's permissions
872
	 *
873
	 * @return string
874
	 * @deprecated since 1.25, use getUser( Revision::RAW )
875
	 */
876
	public function getRawUser() {
877
		wfDeprecated( __METHOD__, '1.25' );
878
		return $this->getUser( self::RAW );
879
	}
880
881
	/**
882
	 * Fetch revision's username if it's available to the specified audience.
883
	 * If the specified audience does not have access to the username, an
884
	 * empty string will be returned.
885
	 *
886
	 * @param int $audience One of:
887
	 *   Revision::FOR_PUBLIC       to be displayed to all users
888
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
889
	 *   Revision::RAW              get the text regardless of permissions
890
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
891
	 *   to the $audience parameter
892
	 * @return string
893
	 */
894
	public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
895
		$this->loadMutableFields();
896
897
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
898
			return '';
899
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
900
			return '';
901
		} else {
902
			if ( $this->mUserText === null ) {
903
				$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...
904
				if ( $this->mUserText === false ) {
905
					# This shouldn't happen, but it can if the wiki was recovered
906
					# via importing revs and there is no user table entry yet.
907
					$this->mUserText = $this->mOrigUserText;
908
				}
909
			}
910
			return $this->mUserText;
911
		}
912
	}
913
914
	/**
915
	 * Fetch revision's username without regard for view restrictions
916
	 *
917
	 * @return string
918
	 * @deprecated since 1.25, use getUserText( Revision::RAW )
919
	 */
920
	public function getRawUserText() {
921
		wfDeprecated( __METHOD__, '1.25' );
922
		return $this->getUserText( self::RAW );
923
	}
924
925
	/**
926
	 * Fetch revision comment if it's available to the specified audience.
927
	 * If the specified audience does not have access to the comment, an
928
	 * empty string will be returned.
929
	 *
930
	 * @param int $audience One of:
931
	 *   Revision::FOR_PUBLIC       to be displayed to all users
932
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
933
	 *   Revision::RAW              get the text regardless of permissions
934
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
935
	 *   to the $audience parameter
936
	 * @return string
937
	 */
938 View Code Duplication
	function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
939
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
940
			return '';
941
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
942
			return '';
943
		} else {
944
			return $this->mComment;
945
		}
946
	}
947
948
	/**
949
	 * Fetch revision comment without regard for the current user's permissions
950
	 *
951
	 * @return string
952
	 * @deprecated since 1.25, use getComment( Revision::RAW )
953
	 */
954
	public function getRawComment() {
955
		wfDeprecated( __METHOD__, '1.25' );
956
		return $this->getComment( self::RAW );
957
	}
958
959
	/**
960
	 * @return bool
961
	 */
962
	public function isMinor() {
963
		return (bool)$this->mMinorEdit;
964
	}
965
966
	/**
967
	 * @return int Rcid of the unpatrolled row, zero if there isn't one
968
	 */
969
	public function isUnpatrolled() {
970
		if ( $this->mUnpatrolled !== null ) {
971
			return $this->mUnpatrolled;
972
		}
973
		$rc = $this->getRecentChange();
974
		if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
975
			$this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
976
		} else {
977
			$this->mUnpatrolled = 0;
978
		}
979
		return $this->mUnpatrolled;
980
	}
981
982
	/**
983
	 * Get the RC object belonging to the current revision, if there's one
984
	 *
985
	 * @param int $flags (optional) $flags include:
986
	 *      Revision::READ_LATEST  : Select the data from the master
987
	 *
988
	 * @since 1.22
989
	 * @return RecentChange|null
990
	 */
991
	public function getRecentChange( $flags = 0 ) {
992
		$dbr = wfGetDB( DB_REPLICA );
993
994
		list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
995
996
		return RecentChange::newFromConds(
997
			[
998
				'rc_user_text' => $this->getUserText( Revision::RAW ),
999
				'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
1000
				'rc_this_oldid' => $this->getId()
1001
			],
1002
			__METHOD__,
1003
			$dbType
1004
		);
1005
	}
1006
1007
	/**
1008
	 * @param int $field One of DELETED_* bitfield constants
1009
	 *
1010
	 * @return bool
1011
	 */
1012
	public function isDeleted( $field ) {
1013
		if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
1014
			// Current revisions of pages cannot have the content hidden. Skipping this
1015
			// check is very useful for Parser as it fetches templates using newKnownCurrent().
1016
			// Calling getVisibility() in that case triggers a verification database query.
1017
			return false; // no need to check
1018
		}
1019
1020
		return ( $this->getVisibility() & $field ) == $field;
1021
	}
1022
1023
	/**
1024
	 * Get the deletion bitfield of the revision
1025
	 *
1026
	 * @return int
1027
	 */
1028
	public function getVisibility() {
1029
		$this->loadMutableFields();
1030
1031
		return (int)$this->mDeleted;
1032
	}
1033
1034
	/**
1035
	 * Fetch revision text if it's available to the specified audience.
1036
	 * If the specified audience does not have the ability to view this
1037
	 * revision, an empty string will be returned.
1038
	 *
1039
	 * @param int $audience One of:
1040
	 *   Revision::FOR_PUBLIC       to be displayed to all users
1041
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
1042
	 *   Revision::RAW              get the text regardless of permissions
1043
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1044
	 *   to the $audience parameter
1045
	 *
1046
	 * @deprecated since 1.21, use getContent() instead
1047
	 * @todo Replace usage in core
1048
	 * @return string
1049
	 */
1050
	public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
1051
		ContentHandler::deprecated( __METHOD__, '1.21' );
1052
1053
		$content = $this->getContent( $audience, $user );
1054
		return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
1055
	}
1056
1057
	/**
1058
	 * Fetch revision content if it's available to the specified audience.
1059
	 * If the specified audience does not have the ability to view this
1060
	 * revision, null will be returned.
1061
	 *
1062
	 * @param int $audience One of:
1063
	 *   Revision::FOR_PUBLIC       to be displayed to all users
1064
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
1065
	 *   Revision::RAW              get the text regardless of permissions
1066
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1067
	 *   to the $audience parameter
1068
	 * @since 1.21
1069
	 * @return Content|null
1070
	 */
1071
	public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
1072
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
1073
			return null;
1074
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
1075
			return null;
1076
		} else {
1077
			return $this->getContentInternal();
1078
		}
1079
	}
1080
1081
	/**
1082
	 * Fetch original serialized data without regard for view restrictions
1083
	 *
1084
	 * @since 1.21
1085
	 * @return string
1086
	 */
1087
	public function getSerializedData() {
1088
		if ( $this->mText === null ) {
1089
			$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...
1090
		}
1091
1092
		return $this->mText;
1093
	}
1094
1095
	/**
1096
	 * Gets the content object for the revision (or null on failure).
1097
	 *
1098
	 * Note that for mutable Content objects, each call to this method will return a
1099
	 * fresh clone.
1100
	 *
1101
	 * @since 1.21
1102
	 * @return Content|null The Revision's content, or null on failure.
1103
	 */
1104
	protected function getContentInternal() {
1105
		if ( $this->mContent === null ) {
1106
			// Revision is immutable. Load on demand:
1107
			if ( $this->mText === null ) {
1108
				$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...
1109
			}
1110
1111
			if ( $this->mText !== null && $this->mText !== false ) {
1112
				// Unserialize content
1113
				$handler = $this->getContentHandler();
1114
				$format = $this->getContentFormat();
1115
1116
				$this->mContent = $handler->unserializeContent( $this->mText, $format );
0 ignored issues
show
Bug introduced by
It seems like $this->mText 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...
1117
			}
1118
		}
1119
1120
		// NOTE: copy() will return $this for immutable content objects
1121
		return $this->mContent ? $this->mContent->copy() : null;
1122
	}
1123
1124
	/**
1125
	 * Returns the content model for this revision.
1126
	 *
1127
	 * If no content model was stored in the database, the default content model for the title is
1128
	 * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
1129
	 * is used as a last resort.
1130
	 *
1131
	 * @return string The content model id associated with this revision,
1132
	 *     see the CONTENT_MODEL_XXX constants.
1133
	 **/
1134
	public function getContentModel() {
1135
		if ( !$this->mContentModel ) {
1136
			$title = $this->getTitle();
1137
			if ( $title ) {
1138
				$this->mContentModel = ContentHandler::getDefaultModelFor( $title );
1139
			} else {
1140
				$this->mContentModel = CONTENT_MODEL_WIKITEXT;
1141
			}
1142
1143
			assert( !empty( $this->mContentModel ) );
1144
		}
1145
1146
		return $this->mContentModel;
1147
	}
1148
1149
	/**
1150
	 * Returns the content format for this revision.
1151
	 *
1152
	 * If no content format was stored in the database, the default format for this
1153
	 * revision's content model is returned.
1154
	 *
1155
	 * @return string The content format id associated with this revision,
1156
	 *     see the CONTENT_FORMAT_XXX constants.
1157
	 **/
1158
	public function getContentFormat() {
1159
		if ( !$this->mContentFormat ) {
1160
			$handler = $this->getContentHandler();
1161
			$this->mContentFormat = $handler->getDefaultFormat();
1162
1163
			assert( !empty( $this->mContentFormat ) );
1164
		}
1165
1166
		return $this->mContentFormat;
1167
	}
1168
1169
	/**
1170
	 * Returns the content handler appropriate for this revision's content model.
1171
	 *
1172
	 * @throws MWException
1173
	 * @return ContentHandler
1174
	 */
1175
	public function getContentHandler() {
1176
		if ( !$this->mContentHandler ) {
1177
			$model = $this->getContentModel();
1178
			$this->mContentHandler = ContentHandler::getForModelID( $model );
1179
1180
			$format = $this->getContentFormat();
1181
1182
			if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
1183
				throw new MWException( "Oops, the content format $format is not supported for "
1184
					. "this content model, $model" );
1185
			}
1186
		}
1187
1188
		return $this->mContentHandler;
1189
	}
1190
1191
	/**
1192
	 * @return string
1193
	 */
1194
	public function getTimestamp() {
1195
		return wfTimestamp( TS_MW, $this->mTimestamp );
1196
	}
1197
1198
	/**
1199
	 * @return bool
1200
	 */
1201
	public function isCurrent() {
1202
		return $this->mCurrent;
1203
	}
1204
1205
	/**
1206
	 * Get previous revision for this title
1207
	 *
1208
	 * @return Revision|null
1209
	 */
1210 View Code Duplication
	public function getPrevious() {
1211
		if ( $this->getTitle() ) {
1212
			$prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
1213
			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...
1214
				return self::newFromTitle( $this->getTitle(), $prev );
1215
			}
1216
		}
1217
		return null;
1218
	}
1219
1220
	/**
1221
	 * Get next revision for this title
1222
	 *
1223
	 * @return Revision|null
1224
	 */
1225 View Code Duplication
	public function getNext() {
1226
		if ( $this->getTitle() ) {
1227
			$next = $this->getTitle()->getNextRevisionID( $this->getId() );
1228
			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...
1229
				return self::newFromTitle( $this->getTitle(), $next );
1230
			}
1231
		}
1232
		return null;
1233
	}
1234
1235
	/**
1236
	 * Get previous revision Id for this page_id
1237
	 * This is used to populate rev_parent_id on save
1238
	 *
1239
	 * @param IDatabase $db
1240
	 * @return int
1241
	 */
1242
	private function getPreviousRevisionId( $db ) {
1243
		if ( $this->mPage === null ) {
1244
			return 0;
1245
		}
1246
		# Use page_latest if ID is not given
1247
		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...
1248
			$prevId = $db->selectField( 'page', 'page_latest',
1249
				[ 'page_id' => $this->mPage ],
1250
				__METHOD__ );
1251
		} else {
1252
			$prevId = $db->selectField( 'revision', 'rev_id',
1253
				[ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
1254
				__METHOD__,
1255
				[ 'ORDER BY' => 'rev_id DESC' ] );
1256
		}
1257
		return intval( $prevId );
1258
	}
1259
1260
	/**
1261
	 * Get revision text associated with an old or archive row
1262
	 * $row is usually an object from wfFetchRow(), both the flags and the text
1263
	 * field must be included.
1264
	 *
1265
	 * @param stdClass $row The text data
1266
	 * @param string $prefix Table prefix (default 'old_')
1267
	 * @param string|bool $wiki The name of the wiki to load the revision text from
1268
	 *   (same as the the wiki $row was loaded from) or false to indicate the local
1269
	 *   wiki (this is the default). Otherwise, it must be a symbolic wiki database
1270
	 *   identifier as understood by the LoadBalancer class.
1271
	 * @return string Text the text requested or false on failure
1272
	 */
1273
	public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
1274
1275
		# Get data
1276
		$textField = $prefix . 'text';
1277
		$flagsField = $prefix . 'flags';
1278
1279
		if ( isset( $row->$flagsField ) ) {
1280
			$flags = explode( ',', $row->$flagsField );
1281
		} else {
1282
			$flags = [];
1283
		}
1284
1285
		if ( isset( $row->$textField ) ) {
1286
			$text = $row->$textField;
1287
		} else {
1288
			return false;
1289
		}
1290
1291
		# Use external methods for external objects, text in table is URL-only then
1292
		if ( in_array( 'external', $flags ) ) {
1293
			$url = $text;
1294
			$parts = explode( '://', $url, 2 );
1295
			if ( count( $parts ) == 1 || $parts[1] == '' ) {
1296
				return false;
1297
			}
1298
			$text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
1299
		}
1300
1301
		// If the text was fetched without an error, convert it
1302
		if ( $text !== false ) {
1303
			$text = self::decompressRevisionText( $text, $flags );
1304
		}
1305
		return $text;
1306
	}
1307
1308
	/**
1309
	 * If $wgCompressRevisions is enabled, we will compress data.
1310
	 * The input string is modified in place.
1311
	 * Return value is the flags field: contains 'gzip' if the
1312
	 * data is compressed, and 'utf-8' if we're saving in UTF-8
1313
	 * mode.
1314
	 *
1315
	 * @param mixed $text Reference to a text
1316
	 * @return string
1317
	 */
1318
	public static function compressRevisionText( &$text ) {
1319
		global $wgCompressRevisions;
1320
		$flags = [];
1321
1322
		# Revisions not marked this way will be converted
1323
		# on load if $wgLegacyCharset is set in the future.
1324
		$flags[] = 'utf-8';
1325
1326
		if ( $wgCompressRevisions ) {
1327
			if ( function_exists( 'gzdeflate' ) ) {
1328
				$deflated = gzdeflate( $text );
1329
1330
				if ( $deflated === false ) {
1331
					wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
1332
				} else {
1333
					$text = $deflated;
1334
					$flags[] = 'gzip';
1335
				}
1336
			} else {
1337
				wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
1338
			}
1339
		}
1340
		return implode( ',', $flags );
1341
	}
1342
1343
	/**
1344
	 * Re-converts revision text according to it's flags.
1345
	 *
1346
	 * @param mixed $text Reference to a text
1347
	 * @param array $flags Compression flags
1348
	 * @return string|bool Decompressed text, or false on failure
1349
	 */
1350
	public static function decompressRevisionText( $text, $flags ) {
1351
		if ( in_array( 'gzip', $flags ) ) {
1352
			# Deal with optional compression of archived pages.
1353
			# This can be done periodically via maintenance/compressOld.php, and
1354
			# as pages are saved if $wgCompressRevisions is set.
1355
			$text = gzinflate( $text );
1356
1357
			if ( $text === false ) {
1358
				wfLogWarning( __METHOD__ . ': gzinflate() failed' );
1359
				return false;
1360
			}
1361
		}
1362
1363
		if ( in_array( 'object', $flags ) ) {
1364
			# Generic compressed storage
1365
			$obj = unserialize( $text );
1366
			if ( !is_object( $obj ) ) {
1367
				// Invalid object
1368
				return false;
1369
			}
1370
			$text = $obj->getText();
1371
		}
1372
1373
		global $wgLegacyEncoding;
1374
		if ( $text !== false && $wgLegacyEncoding
1375
			&& !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
1376
		) {
1377
			# Old revisions kept around in a legacy encoding?
1378
			# Upconvert on demand.
1379
			# ("utf8" checked for compatibility with some broken
1380
			#  conversion scripts 2008-12-30)
1381
			global $wgContLang;
1382
			$text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
1383
		}
1384
1385
		return $text;
1386
	}
1387
1388
	/**
1389
	 * Insert a new revision into the database, returning the new revision ID
1390
	 * number on success and dies horribly on failure.
1391
	 *
1392
	 * @param IDatabase $dbw (master connection)
1393
	 * @throws MWException
1394
	 * @return int
1395
	 */
1396
	public function insertOn( $dbw ) {
1397
		global $wgDefaultExternalStore, $wgContentHandlerUseDB;
1398
1399
		// Not allowed to have rev_page equal to 0, false, etc.
1400 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...
1401
			$title = $this->getTitle();
1402
			if ( $title instanceof Title ) {
1403
				$titleText = ' for page ' . $title->getPrefixedText();
1404
			} else {
1405
				$titleText = '';
1406
			}
1407
			throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
1408
		}
1409
1410
		$this->checkContentModel();
1411
1412
		$data = $this->mText;
1413
		$flags = self::compressRevisionText( $data );
1414
1415
		# Write to external storage if required
1416
		if ( $wgDefaultExternalStore ) {
1417
			// Store and get the URL
1418
			$data = ExternalStore::insertToDefault( $data );
1419
			if ( !$data ) {
1420
				throw new MWException( "Unable to store text to external storage" );
1421
			}
1422
			if ( $flags ) {
1423
				$flags .= ',';
1424
			}
1425
			$flags .= 'external';
1426
		}
1427
1428
		# Record the text (or external storage URL) to the text table
1429
		if ( $this->mTextId === null ) {
1430
			$old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
1431
			$dbw->insert( 'text',
1432
				[
1433
					'old_id' => $old_id,
1434
					'old_text' => $data,
1435
					'old_flags' => $flags,
1436
				], __METHOD__
1437
			);
1438
			$this->mTextId = $dbw->insertId();
1439
		}
1440
1441
		if ( $this->mComment === null ) {
1442
			$this->mComment = "";
1443
		}
1444
1445
		# Record the edit in revisions
1446
		$rev_id = $this->mId !== null
1447
			? $this->mId
1448
			: $dbw->nextSequenceValue( 'revision_rev_id_seq' );
1449
		$row = [
1450
			'rev_id'         => $rev_id,
1451
			'rev_page'       => $this->mPage,
1452
			'rev_text_id'    => $this->mTextId,
1453
			'rev_comment'    => $this->mComment,
1454
			'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
1455
			'rev_user'       => $this->mUser,
1456
			'rev_user_text'  => $this->mUserText,
1457
			'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
1458
			'rev_deleted'    => $this->mDeleted,
1459
			'rev_len'        => $this->mSize,
1460
			'rev_parent_id'  => $this->mParentId === null
1461
				? $this->getPreviousRevisionId( $dbw )
1462
				: $this->mParentId,
1463
			'rev_sha1'       => $this->mSha1 === null
1464
				? Revision::base36Sha1( $this->mText )
1465
				: $this->mSha1,
1466
		];
1467
1468
		if ( $wgContentHandlerUseDB ) {
1469
			// NOTE: Store null for the default model and format, to save space.
1470
			// XXX: Makes the DB sensitive to changed defaults.
1471
			// Make this behavior optional? Only in miser mode?
1472
1473
			$model = $this->getContentModel();
1474
			$format = $this->getContentFormat();
1475
1476
			$title = $this->getTitle();
1477
1478
			if ( $title === null ) {
1479
				throw new MWException( "Insufficient information to determine the title of the "
1480
					. "revision's page!" );
1481
			}
1482
1483
			$defaultModel = ContentHandler::getDefaultModelFor( $title );
1484
			$defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
1485
1486
			$row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
1487
			$row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
1488
		}
1489
1490
		$dbw->insert( 'revision', $row, __METHOD__ );
1491
1492
		$this->mId = $rev_id !== null ? $rev_id : $dbw->insertId();
1493
1494
		// Assertion to try to catch T92046
1495
		if ( (int)$this->mId === 0 ) {
1496
			throw new UnexpectedValueException(
1497
				'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
1498
					var_export( $row, 1 )
1499
			);
1500
		}
1501
1502
		Hooks::run( 'RevisionInsertComplete', [ &$this, $data, $flags ] );
1503
1504
		return $this->mId;
1505
	}
1506
1507
	protected function checkContentModel() {
1508
		global $wgContentHandlerUseDB;
1509
1510
		// Note: may return null for revisions that have not yet been inserted
1511
		$title = $this->getTitle();
1512
1513
		$model = $this->getContentModel();
1514
		$format = $this->getContentFormat();
1515
		$handler = $this->getContentHandler();
1516
1517
		if ( !$handler->isSupportedFormat( $format ) ) {
1518
			$t = $title->getPrefixedDBkey();
1519
1520
			throw new MWException( "Can't use format $format with content model $model on $t" );
1521
		}
1522
1523
		if ( !$wgContentHandlerUseDB && $title ) {
1524
			// if $wgContentHandlerUseDB is not set,
1525
			// all revisions must use the default content model and format.
1526
1527
			$defaultModel = ContentHandler::getDefaultModelFor( $title );
1528
			$defaultHandler = ContentHandler::getForModelID( $defaultModel );
1529
			$defaultFormat = $defaultHandler->getDefaultFormat();
1530
1531
			if ( $this->getContentModel() != $defaultModel ) {
1532
				$t = $title->getPrefixedDBkey();
1533
1534
				throw new MWException( "Can't save non-default content model with "
1535
					. "\$wgContentHandlerUseDB disabled: model is $model, "
1536
					. "default for $t is $defaultModel" );
1537
			}
1538
1539
			if ( $this->getContentFormat() != $defaultFormat ) {
1540
				$t = $title->getPrefixedDBkey();
1541
1542
				throw new MWException( "Can't use non-default content format with "
1543
					. "\$wgContentHandlerUseDB disabled: format is $format, "
1544
					. "default for $t is $defaultFormat" );
1545
			}
1546
		}
1547
1548
		$content = $this->getContent( Revision::RAW );
1549
		$prefixedDBkey = $title->getPrefixedDBkey();
1550
		$revId = $this->mId;
1551
1552
		if ( !$content ) {
1553
			throw new MWException(
1554
				"Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
1555
			);
1556
		}
1557
		if ( !$content->isValid() ) {
1558
			throw new MWException(
1559
				"Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
1560
			);
1561
		}
1562
	}
1563
1564
	/**
1565
	 * Get the base 36 SHA-1 value for a string of text
1566
	 * @param string $text
1567
	 * @return string
1568
	 */
1569
	public static function base36Sha1( $text ) {
1570
		return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
1571
	}
1572
1573
	/**
1574
	 * Lazy-load the revision's text.
1575
	 * Currently hardcoded to the 'text' table storage engine.
1576
	 *
1577
	 * @return string|bool The revision's text, or false on failure
1578
	 */
1579
	protected function loadText() {
1580
		// Caching may be beneficial for massive use of external storage
1581
		global $wgRevisionCacheExpiry;
1582
		static $processCache = null;
1583
1584
		if ( !$processCache ) {
1585
			$processCache = new MapCacheLRU( 10 );
1586
		}
1587
1588
		$cache = ObjectCache::getMainWANInstance();
1589
		$textId = $this->getTextId();
1590
		$key = wfMemcKey( 'revisiontext', 'textid', $textId );
1591
1592
		if ( $wgRevisionCacheExpiry ) {
1593
			if ( $processCache->has( $key ) ) {
1594
				return $processCache->get( $key );
1595
			}
1596
			$text = $cache->get( $key );
1597
			if ( is_string( $text ) ) {
1598
				$processCache->set( $key, $text );
1599
				return $text;
1600
			}
1601
		}
1602
1603
		// If we kept data for lazy extraction, use it now...
1604
		if ( $this->mTextRow !== null ) {
1605
			$row = $this->mTextRow;
1606
			$this->mTextRow = null;
1607
		} else {
1608
			$row = null;
1609
		}
1610
1611
		if ( !$row ) {
1612
			// Text data is immutable; check replica DBs first.
1613
			$dbr = wfGetDB( DB_REPLICA );
1614
			$row = $dbr->selectRow( 'text',
1615
				[ 'old_text', 'old_flags' ],
1616
				[ 'old_id' => $textId ],
1617
				__METHOD__ );
1618
		}
1619
1620
		// Fallback to the master in case of replica DB lag. Also use FOR UPDATE if it was
1621
		// used to fetch this revision to avoid missing the row due to REPEATABLE-READ.
1622
		$forUpdate = ( $this->mQueryFlags & self::READ_LOCKING == self::READ_LOCKING );
1623
		if ( !$row && ( $forUpdate || 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...
1624
			$dbw = wfGetDB( DB_MASTER );
1625
			$row = $dbw->selectRow( 'text',
1626
				[ 'old_text', 'old_flags' ],
1627
				[ 'old_id' => $textId ],
1628
				__METHOD__,
1629
				$forUpdate ? [ 'FOR UPDATE' ] : [] );
1630
		}
1631
1632
		if ( !$row ) {
1633
			wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
1634
		}
1635
1636
		$text = self::getRevisionText( $row );
0 ignored issues
show
Bug introduced by
It seems like $row can also be of type boolean; however, Revision::getRevisionText() 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...
1637
		if ( $row && $text === false ) {
1638
			wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
1639
		}
1640
1641
		# No negative caching -- negative hits on text rows may be due to corrupted replica DB servers
1642
		if ( $wgRevisionCacheExpiry && $text !== false ) {
1643
			$processCache->set( $key, $text );
1644
			$cache->set( $key, $text, $wgRevisionCacheExpiry );
1645
		}
1646
1647
		return $text;
1648
	}
1649
1650
	/**
1651
	 * Create a new null-revision for insertion into a page's
1652
	 * history. This will not re-save the text, but simply refer
1653
	 * to the text from the previous version.
1654
	 *
1655
	 * Such revisions can for instance identify page rename
1656
	 * operations and other such meta-modifications.
1657
	 *
1658
	 * @param IDatabase $dbw
1659
	 * @param int $pageId ID number of the page to read from
1660
	 * @param string $summary Revision's summary
1661
	 * @param bool $minor Whether the revision should be considered as minor
1662
	 * @param User|null $user User object to use or null for $wgUser
1663
	 * @return Revision|null Revision or null on error
1664
	 */
1665
	public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
1666
		global $wgContentHandlerUseDB, $wgContLang;
1667
1668
		$fields = [ 'page_latest', 'page_namespace', 'page_title',
1669
						'rev_text_id', 'rev_len', 'rev_sha1' ];
1670
1671
		if ( $wgContentHandlerUseDB ) {
1672
			$fields[] = 'rev_content_model';
1673
			$fields[] = 'rev_content_format';
1674
		}
1675
1676
		$current = $dbw->selectRow(
1677
			[ 'page', 'revision' ],
1678
			$fields,
1679
			[
1680
				'page_id' => $pageId,
1681
				'page_latest=rev_id',
1682
			],
1683
			__METHOD__,
1684
			[ 'FOR UPDATE' ] // T51581
1685
		);
1686
1687
		if ( $current ) {
1688
			if ( !$user ) {
1689
				global $wgUser;
1690
				$user = $wgUser;
1691
			}
1692
1693
			// Truncate for whole multibyte characters
1694
			$summary = $wgContLang->truncate( $summary, 255 );
1695
1696
			$row = [
1697
				'page'       => $pageId,
1698
				'user_text'  => $user->getName(),
1699
				'user'       => $user->getId(),
1700
				'comment'    => $summary,
1701
				'minor_edit' => $minor,
1702
				'text_id'    => $current->rev_text_id,
1703
				'parent_id'  => $current->page_latest,
1704
				'len'        => $current->rev_len,
1705
				'sha1'       => $current->rev_sha1
1706
			];
1707
1708
			if ( $wgContentHandlerUseDB ) {
1709
				$row['content_model'] = $current->rev_content_model;
1710
				$row['content_format'] = $current->rev_content_format;
1711
			}
1712
1713
			$row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
1714
1715
			$revision = new Revision( $row );
1716
		} else {
1717
			$revision = null;
1718
		}
1719
1720
		return $revision;
1721
	}
1722
1723
	/**
1724
	 * Determine if the current user is allowed to view a particular
1725
	 * field of this revision, if it's marked as deleted.
1726
	 *
1727
	 * @param int $field One of self::DELETED_TEXT,
1728
	 *                              self::DELETED_COMMENT,
1729
	 *                              self::DELETED_USER
1730
	 * @param User|null $user User object to check, or null to use $wgUser
1731
	 * @return bool
1732
	 */
1733
	public function userCan( $field, User $user = null ) {
1734
		return self::userCanBitfield( $this->getVisibility(), $field, $user );
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. This is used
1740
	 * by various classes to avoid duplication.
1741
	 *
1742
	 * @param int $bitfield Current field
1743
	 * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
1744
	 *                               self::DELETED_COMMENT = File::DELETED_COMMENT,
1745
	 *                               self::DELETED_USER = File::DELETED_USER
1746
	 * @param User|null $user User object to check, or null to use $wgUser
1747
	 * @param Title|null $title A Title object to check for per-page restrictions on,
1748
	 *                          instead of just plain userrights
1749
	 * @return bool
1750
	 */
1751
	public static function userCanBitfield( $bitfield, $field, User $user = null,
1752
		Title $title = null
1753
	) {
1754
		if ( $bitfield & $field ) { // aspect is deleted
1755
			if ( $user === null ) {
1756
				global $wgUser;
1757
				$user = $wgUser;
1758
			}
1759
			if ( $bitfield & self::DELETED_RESTRICTED ) {
1760
				$permissions = [ 'suppressrevision', 'viewsuppressed' ];
1761
			} elseif ( $field & self::DELETED_TEXT ) {
1762
				$permissions = [ 'deletedtext' ];
1763
			} else {
1764
				$permissions = [ 'deletedhistory' ];
1765
			}
1766
			$permissionlist = implode( ', ', $permissions );
1767
			if ( $title === null ) {
1768
				wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
1769
				return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
1770
			} else {
1771
				$text = $title->getPrefixedText();
1772
				wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
1773
				foreach ( $permissions as $perm ) {
1774
					if ( $title->userCan( $perm, $user ) ) {
1775
						return true;
1776
					}
1777
				}
1778
				return false;
1779
			}
1780
		} else {
1781
			return true;
1782
		}
1783
	}
1784
1785
	/**
1786
	 * Get rev_timestamp from rev_id, without loading the rest of the row
1787
	 *
1788
	 * @param Title $title
1789
	 * @param int $id
1790
	 * @return string|bool False if not found
1791
	 */
1792
	static function getTimestampFromId( $title, $id, $flags = 0 ) {
1793
		$db = ( $flags & self::READ_LATEST )
1794
			? wfGetDB( DB_MASTER )
1795
			: wfGetDB( DB_REPLICA );
1796
		// Casting fix for databases that can't take '' for rev_id
1797
		if ( $id == '' ) {
1798
			$id = 0;
1799
		}
1800
		$conds = [ 'rev_id' => $id ];
1801
		$conds['rev_page'] = $title->getArticleID();
1802
		$timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
1803
1804
		return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
1805
	}
1806
1807
	/**
1808
	 * Get count of revisions per page...not very efficient
1809
	 *
1810
	 * @param IDatabase $db
1811
	 * @param int $id Page id
1812
	 * @return int
1813
	 */
1814
	static function countByPageId( $db, $id ) {
1815
		$row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
1816
			[ 'rev_page' => $id ], __METHOD__ );
1817
		if ( $row ) {
1818
			return $row->revCount;
1819
		}
1820
		return 0;
1821
	}
1822
1823
	/**
1824
	 * Get count of revisions per page...not very efficient
1825
	 *
1826
	 * @param IDatabase $db
1827
	 * @param Title $title
1828
	 * @return int
1829
	 */
1830
	static function countByTitle( $db, $title ) {
1831
		$id = $title->getArticleID();
1832
		if ( $id ) {
1833
			return self::countByPageId( $db, $id );
1834
		}
1835
		return 0;
1836
	}
1837
1838
	/**
1839
	 * Check if no edits were made by other users since
1840
	 * the time a user started editing the page. Limit to
1841
	 * 50 revisions for the sake of performance.
1842
	 *
1843
	 * @since 1.20
1844
	 * @deprecated since 1.24
1845
	 *
1846
	 * @param IDatabase|int $db The Database to perform the check on. May be given as a
1847
	 *        Database object or a database identifier usable with wfGetDB.
1848
	 * @param int $pageId The ID of the page in question
1849
	 * @param int $userId The ID of the user in question
1850
	 * @param string $since Look at edits since this time
1851
	 *
1852
	 * @return bool True if the given user was the only one to edit since the given timestamp
1853
	 */
1854
	public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
1855
		if ( !$userId ) {
1856
			return false;
1857
		}
1858
1859
		if ( is_int( $db ) ) {
1860
			$db = wfGetDB( $db );
1861
		}
1862
1863
		$res = $db->select( 'revision',
1864
			'rev_user',
1865
			[
1866
				'rev_page' => $pageId,
1867
				'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
1868
			],
1869
			__METHOD__,
1870
			[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
1871
		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...
1872
			if ( $row->rev_user != $userId ) {
1873
				return false;
1874
			}
1875
		}
1876
		return true;
1877
	}
1878
1879
	/**
1880
	 * Load a revision based on a known page ID and current revision ID from the DB
1881
	 *
1882
	 * This method allows for the use of caching, though accessing anything that normally
1883
	 * requires permission checks (aside from the text) will trigger a small DB lookup.
1884
	 * The title will also be lazy loaded, though setTitle() can be used to preload it.
1885
	 *
1886
	 * @param IDatabase $db
1887
	 * @param int $pageId Page ID
1888
	 * @param int $revId Known current revision of this page
1889
	 * @return Revision|bool Returns false if missing
1890
	 * @since 1.28
1891
	 */
1892
	public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
1893
		$cache = ObjectCache::getMainWANInstance();
1894
		return $cache->getWithSetCallback(
1895
			// Page/rev IDs passed in from DB to reflect history merges
1896
			$cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ),
1897
			$cache::TTL_WEEK,
1898
			function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
1899
				$setOpts += Database::getCacheSetOptions( $db );
1900
1901
				$rev = Revision::loadFromPageId( $db, $pageId, $revId );
1902
				// Reflect revision deletion and user renames
1903
				if ( $rev ) {
1904
					$rev->mTitle = null; // mutable; lazy-load
1905
					$rev->mRefreshMutableFields = true;
1906
				}
1907
1908
				return $rev ?: false; // don't cache negatives
1909
			}
1910
		);
1911
	}
1912
1913
	/**
1914
	 * For cached revisions, make sure the user name and rev_deleted is up-to-date
1915
	 */
1916
	private function loadMutableFields() {
1917
		if ( !$this->mRefreshMutableFields ) {
1918
			return; // not needed
1919
		}
1920
1921
		$this->mRefreshMutableFields = false;
1922
		$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...
1923
		$row = $dbr->selectRow(
1924
			[ 'revision', 'user' ],
1925
			[ 'rev_deleted', 'user_name' ],
1926
			[ 'rev_id' => $this->mId, 'user_id = rev_user' ],
1927
			__METHOD__
1928
		);
1929
		if ( $row ) { // update values
1930
			$this->mDeleted = (int)$row->rev_deleted;
1931
			$this->mUserText = $row->user_name;
1932
		}
1933
	}
1934
}
1935