Completed
Branch master (771964)
by
unknown
26:13
created

Revision   F

Complexity

Total Complexity 261

Size/Duplication

Total Lines 1824
Duplicated Lines 4 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 261
lcom 1
cbo 17
dl 73
loc 1824
rs 0.5217
c 1
b 0
f 0

69 Methods

Rating   Name   Duplication   Size   Complexity  
A newFromId() 0 3 1
A newFromTitle() 16 16 3
A newFromPageId() 12 12 3
F newFromArchiveRow() 0 40 14
A newFromRow() 0 3 1
A loadFromId() 0 3 1
A loadFromPageId() 0 9 2
C newFromConds() 0 22 7
A loadFromConds() 0 12 3
A fetchFromConds() 0 19 2
A userJoinCond() 0 3 1
A pageJoinCond() 0 3 1
B selectFields() 0 25 2
B selectArchiveFields() 0 25 2
A selectTextFields() 0 6 1
A selectPageFields() 0 10 1
A selectUserFields() 0 3 1
A getParentLengths() 0 14 3
F __construct() 0 161 41
A getId() 0 3 1
A setId() 0 3 1
A getTextId() 0 3 1
A getParentId() 0 3 1
A getSize() 0 3 1
A getSha1() 0 3 1
C getTitle() 0 24 7
A setTitle() 0 3 1
A getPage() 0 3 1
B getUser() 9 9 5
A getRawUser() 0 4 1
B getUserText() 0 17 7
A getRawUserText() 0 4 1
B getComment() 9 9 5
A getRawComment() 0 4 1
A isMinor() 0 3 1
A isUnpatrolled() 0 12 4
A isDeleted() 0 3 1
A getVisibility() 0 3 1
A getText() 0 6 1
B getContent() 0 9 5
A getRawText() 0 4 1
A getSerializedData() 0 7 2
B getContentInternal() 0 19 6
A getContentModel() 0 14 3
A getContentFormat() 0 10 2
A getContentHandler() 0 15 3
A getTimestamp() 0 3 1
A isCurrent() 0 3 1
A getPrevious() 9 9 3
A getNext() 9 9 3
A getPreviousRevisionId() 0 17 3
C getRevisionText() 0 34 7
B compressRevisionText() 0 24 4
D decompressRevisionText() 0 37 9
B checkContentModel() 0 56 8
A base36Sha1() 0 3 1
A loadFromTitle() 0 14 2
A loadFromTimestamp() 0 9 1
A fetchRevision() 0 10 1
A getRecentChange() 0 15 1
F insertOn() 9 110 18
D loadText() 0 71 16
B newNullRevision() 0 57 5
A userCan() 0 3 1
C userCanBitfield() 0 33 8
A getTimestampFromId() 0 14 4
A countByPageId() 0 8 2
A countByTitle() 0 7 2
B userWasLastToEdit() 0 24 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Revision often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Revision, and based on these observations, apply Extract Interface, too.

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
23
/**
24
 * @todo document
25
 */
26
class Revision implements IDBAccessObject {
27
	protected $mId;
28
29
	/**
30
	 * @var int|null
31
	 */
32
	protected $mPage;
33
	protected $mUserText;
34
	protected $mOrigUserText;
35
	protected $mUser;
36
	protected $mMinorEdit;
37
	protected $mTimestamp;
38
	protected $mDeleted;
39
	protected $mSize;
40
	protected $mSha1;
41
	protected $mParentId;
42
	protected $mComment;
43
	protected $mText;
44
	protected $mTextId;
45
46
	/**
47
	 * @var stdClass|null
48
	 */
49
	protected $mTextRow;
50
51
	/**
52
	 * @var null|Title
53
	 */
54
	protected $mTitle;
55
	protected $mCurrent;
56
	protected $mContentModel;
57
	protected $mContentFormat;
58
59
	/**
60
	 * @var Content|null|bool
61
	 */
62
	protected $mContent;
63
64
	/**
65
	 * @var null|ContentHandler
66
	 */
67
	protected $mContentHandler;
68
69
	/**
70
	 * @var int
71
	 */
72
	protected $mQueryFlags = 0;
73
74
	// Revision deletion constants
75
	const DELETED_TEXT = 1;
76
	const DELETED_COMMENT = 2;
77
	const DELETED_USER = 4;
78
	const DELETED_RESTRICTED = 8;
79
	const SUPPRESSED_USER = 12; // convenience
80
81
	// Audience options for accessors
82
	const FOR_PUBLIC = 1;
83
	const FOR_THIS_USER = 2;
84
	const RAW = 3;
85
86
	/**
87
	 * Load a page revision from a given revision ID number.
88
	 * Returns null if no such revision can be found.
89
	 *
90
	 * $flags include:
91
	 *      Revision::READ_LATEST  : Select the data from the master
92
	 *      Revision::READ_LOCKING : Select & lock the data from the master
93
	 *
94
	 * @param int $id
95
	 * @param int $flags (optional)
96
	 * @return Revision|null
97
	 */
98
	public static function newFromId( $id, $flags = 0 ) {
99
		return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
100
	}
101
102
	/**
103
	 * Load either the current, or a specified, revision
104
	 * that's attached to a given link target. If not attached
105
	 * to that link target, will return null.
106
	 *
107
	 * $flags include:
108
	 *      Revision::READ_LATEST  : Select the data from the master
109
	 *      Revision::READ_LOCKING : Select & lock the data from the master
110
	 *
111
	 * @param LinkTarget $linkTarget
112
	 * @param int $id (optional)
113
	 * @param int $flags Bitfield (optional)
114
	 * @return Revision|null
115
	 */
116 View Code Duplication
	public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
117
		$conds = [
118
			'page_namespace' => $linkTarget->getNamespace(),
119
			'page_title' => $linkTarget->getDBkey()
120
		];
121
		if ( $id ) {
122
			// Use the specified ID
123
			$conds['rev_id'] = $id;
124
			return self::newFromConds( $conds, $flags );
125
		} else {
126
			// Use a join to get the latest revision
127
			$conds[] = 'rev_id=page_latest';
128
			$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
129
			return self::loadFromConds( $db, $conds, $flags );
130
		}
131
	}
132
133
	/**
134
	 * Load either the current, or a specified, revision
135
	 * that's attached to a given page ID.
136
	 * Returns null if no such revision can be found.
137
	 *
138
	 * $flags include:
139
	 *      Revision::READ_LATEST  : Select the data from the master (since 1.20)
140
	 *      Revision::READ_LOCKING : Select & lock the data from the master
141
	 *
142
	 * @param int $pageId
143
	 * @param int $revId (optional)
144
	 * @param int $flags Bitfield (optional)
145
	 * @return Revision|null
146
	 */
147 View Code Duplication
	public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
148
		$conds = [ 'page_id' => $pageId ];
149
		if ( $revId ) {
150
			$conds['rev_id'] = $revId;
151
			return self::newFromConds( $conds, $flags );
152
		} else {
153
			// Use a join to get the latest revision
154
			$conds[] = 'rev_id = page_latest';
155
			$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
156
			return self::loadFromConds( $db, $conds, $flags );
157
		}
158
	}
159
160
	/**
161
	 * Make a fake revision object from an archive table row. This is queried
162
	 * for permissions or even inserted (as in Special:Undelete)
163
	 * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
164
	 *
165
	 * @param object $row
166
	 * @param array $overrides
167
	 *
168
	 * @throws MWException
169
	 * @return Revision
170
	 */
171
	public static function newFromArchiveRow( $row, $overrides = [] ) {
172
		global $wgContentHandlerUseDB;
173
174
		$attribs = $overrides + [
175
			'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
176
			'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
177
			'comment'    => $row->ar_comment,
178
			'user'       => $row->ar_user,
179
			'user_text'  => $row->ar_user_text,
180
			'timestamp'  => $row->ar_timestamp,
181
			'minor_edit' => $row->ar_minor_edit,
182
			'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
183
			'deleted'    => $row->ar_deleted,
184
			'len'        => $row->ar_len,
185
			'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
186
			'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
187
			'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
188
		];
189
190
		if ( !$wgContentHandlerUseDB ) {
191
			unset( $attribs['content_model'] );
192
			unset( $attribs['content_format'] );
193
		}
194
195
		if ( !isset( $attribs['title'] )
196
			&& isset( $row->ar_namespace )
197
			&& isset( $row->ar_title )
198
		) {
199
			$attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
200
		}
201
202
		if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
203
			// Pre-1.5 ar_text row
204
			$attribs['text'] = self::getRevisionText( $row, 'ar_' );
205
			if ( $attribs['text'] === false ) {
206
				throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' );
207
			}
208
		}
209
		return new self( $attribs );
210
	}
211
212
	/**
213
	 * @since 1.19
214
	 *
215
	 * @param object $row
216
	 * @return Revision
217
	 */
218
	public static function newFromRow( $row ) {
219
		return new self( $row );
220
	}
221
222
	/**
223
	 * Load a page revision from a given revision ID number.
224
	 * Returns null if no such revision can be found.
225
	 *
226
	 * @param IDatabase $db
227
	 * @param int $id
228
	 * @return Revision|null
229
	 */
230
	public static function loadFromId( $db, $id ) {
231
		return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
232
	}
233
234
	/**
235
	 * Load either the current, or a specified, revision
236
	 * that's attached to a given page. If not attached
237
	 * to that page, will return null.
238
	 *
239
	 * @param IDatabase $db
240
	 * @param int $pageid
241
	 * @param int $id
242
	 * @return Revision|null
243
	 */
244
	public static function loadFromPageId( $db, $pageid, $id = 0 ) {
245
		$conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
246
		if ( $id ) {
247
			$conds['rev_id'] = intval( $id );
248
		} else {
249
			$conds[] = 'rev_id=page_latest';
250
		}
251
		return self::loadFromConds( $db, $conds );
252
	}
253
254
	/**
255
	 * Load either the current, or a specified, revision
256
	 * that's attached to a given page. If not attached
257
	 * to that page, will return null.
258
	 *
259
	 * @param IDatabase $db
260
	 * @param Title $title
261
	 * @param int $id
262
	 * @return Revision|null
263
	 */
264
	public static function loadFromTitle( $db, $title, $id = 0 ) {
265
		if ( $id ) {
266
			$matchId = intval( $id );
267
		} else {
268
			$matchId = 'page_latest';
269
		}
270
		return self::loadFromConds( $db,
271
			[
272
				"rev_id=$matchId",
273
				'page_namespace' => $title->getNamespace(),
274
				'page_title' => $title->getDBkey()
275
			]
276
		);
277
	}
278
279
	/**
280
	 * Load the revision for the given title with the given timestamp.
281
	 * WARNING: Timestamps may in some circumstances not be unique,
282
	 * so this isn't the best key to use.
283
	 *
284
	 * @param IDatabase $db
285
	 * @param Title $title
286
	 * @param string $timestamp
287
	 * @return Revision|null
288
	 */
289
	public static function loadFromTimestamp( $db, $title, $timestamp ) {
290
		return self::loadFromConds( $db,
291
			[
292
				'rev_timestamp' => $db->timestamp( $timestamp ),
293
				'page_namespace' => $title->getNamespace(),
294
				'page_title' => $title->getDBkey()
295
			]
296
		);
297
	}
298
299
	/**
300
	 * Given a set of conditions, fetch a revision
301
	 *
302
	 * This method is used then a revision ID is qualified and
303
	 * will incorporate some basic slave/master fallback logic
304
	 *
305
	 * @param array $conditions
306
	 * @param int $flags (optional)
307
	 * @return Revision|null
308
	 */
309
	private static function newFromConds( $conditions, $flags = 0 ) {
310
		$db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE );
311
312
		$rev = self::loadFromConds( $db, $conditions, $flags );
313
		// Make sure new pending/committed revision are visibile later on
314
		// within web requests to certain avoid bugs like T93866 and T94407.
315
		if ( !$rev
316
			&& !( $flags & self::READ_LATEST )
317
			&& wfGetLB()->getServerCount() > 1
318
			&& wfGetLB()->hasOrMadeRecentMasterChanges()
319
		) {
320
			$flags = self::READ_LATEST;
321
			$db = wfGetDB( DB_MASTER );
322
			$rev = self::loadFromConds( $db, $conditions, $flags );
323
		}
324
325
		if ( $rev ) {
326
			$rev->mQueryFlags = $flags;
327
		}
328
329
		return $rev;
330
	}
331
332
	/**
333
	 * Given a set of conditions, fetch a revision from
334
	 * the given database connection.
335
	 *
336
	 * @param IDatabase $db
337
	 * @param array $conditions
338
	 * @param int $flags (optional)
339
	 * @return Revision|null
340
	 */
341
	private static function loadFromConds( $db, $conditions, $flags = 0 ) {
342
		$res = self::fetchFromConds( $db, $conditions, $flags );
343
		if ( $res ) {
344
			$row = $res->fetchObject();
345
			if ( $row ) {
346
				$ret = new Revision( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $res->fetchObject() on line 344 can also be of type boolean; however, Revision::__construct() does only seem to accept object|array, 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...
347
				return $ret;
348
			}
349
		}
350
		$ret = null;
351
		return $ret;
352
	}
353
354
	/**
355
	 * Return a wrapper for a series of database rows to
356
	 * fetch all of a given page's revisions in turn.
357
	 * Each row can be fed to the constructor to get objects.
358
	 *
359
	 * @param Title $title
360
	 * @return ResultWrapper
361
	 */
362
	public static function fetchRevision( $title ) {
363
		return self::fetchFromConds(
364
			wfGetDB( DB_SLAVE ),
365
			[
366
				'rev_id=page_latest',
367
				'page_namespace' => $title->getNamespace(),
368
				'page_title' => $title->getDBkey()
369
			]
370
		);
371
	}
372
373
	/**
374
	 * Given a set of conditions, return a ResultWrapper
375
	 * which will return matching database rows with the
376
	 * fields necessary to build Revision objects.
377
	 *
378
	 * @param IDatabase $db
379
	 * @param array $conditions
380
	 * @param int $flags (optional)
381
	 * @return ResultWrapper
382
	 */
383
	private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
384
		$fields = array_merge(
385
			self::selectFields(),
386
			self::selectPageFields(),
387
			self::selectUserFields()
388
		);
389
		$options = [ 'LIMIT' => 1 ];
390
		if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
391
			$options[] = 'FOR UPDATE';
392
		}
393
		return $db->select(
394
			[ 'revision', 'page', 'user' ],
395
			$fields,
396
			$conditions,
397
			__METHOD__,
398
			$options,
399
			[ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ]
400
		);
401
	}
402
403
	/**
404
	 * Return the value of a select() JOIN conds array for the user table.
405
	 * This will get user table rows for logged-in users.
406
	 * @since 1.19
407
	 * @return array
408
	 */
409
	public static function userJoinCond() {
410
		return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
411
	}
412
413
	/**
414
	 * Return the value of a select() page conds array for the page table.
415
	 * This will assure that the revision(s) are not orphaned from live pages.
416
	 * @since 1.19
417
	 * @return array
418
	 */
419
	public static function pageJoinCond() {
420
		return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
421
	}
422
423
	/**
424
	 * Return the list of revision fields that should be selected to create
425
	 * a new revision.
426
	 * @return array
427
	 */
428
	public static function selectFields() {
429
		global $wgContentHandlerUseDB;
430
431
		$fields = [
432
			'rev_id',
433
			'rev_page',
434
			'rev_text_id',
435
			'rev_timestamp',
436
			'rev_comment',
437
			'rev_user_text',
438
			'rev_user',
439
			'rev_minor_edit',
440
			'rev_deleted',
441
			'rev_len',
442
			'rev_parent_id',
443
			'rev_sha1',
444
		];
445
446
		if ( $wgContentHandlerUseDB ) {
447
			$fields[] = 'rev_content_format';
448
			$fields[] = 'rev_content_model';
449
		}
450
451
		return $fields;
452
	}
453
454
	/**
455
	 * Return the list of revision fields that should be selected to create
456
	 * a new revision from an archive row.
457
	 * @return array
458
	 */
459
	public static function selectArchiveFields() {
460
		global $wgContentHandlerUseDB;
461
		$fields = [
462
			'ar_id',
463
			'ar_page_id',
464
			'ar_rev_id',
465
			'ar_text',
466
			'ar_text_id',
467
			'ar_timestamp',
468
			'ar_comment',
469
			'ar_user_text',
470
			'ar_user',
471
			'ar_minor_edit',
472
			'ar_deleted',
473
			'ar_len',
474
			'ar_parent_id',
475
			'ar_sha1',
476
		];
477
478
		if ( $wgContentHandlerUseDB ) {
479
			$fields[] = 'ar_content_format';
480
			$fields[] = 'ar_content_model';
481
		}
482
		return $fields;
483
	}
484
485
	/**
486
	 * Return the list of text fields that should be selected to read the
487
	 * revision text
488
	 * @return array
489
	 */
490
	public static function selectTextFields() {
491
		return [
492
			'old_text',
493
			'old_flags'
494
		];
495
	}
496
497
	/**
498
	 * Return the list of page fields that should be selected from page table
499
	 * @return array
500
	 */
501
	public static function selectPageFields() {
502
		return [
503
			'page_namespace',
504
			'page_title',
505
			'page_id',
506
			'page_latest',
507
			'page_is_redirect',
508
			'page_len',
509
		];
510
	}
511
512
	/**
513
	 * Return the list of user fields that should be selected from user table
514
	 * @return array
515
	 */
516
	public static function selectUserFields() {
517
		return [ 'user_name' ];
518
	}
519
520
	/**
521
	 * Do a batched query to get the parent revision lengths
522
	 * @param IDatabase $db
523
	 * @param array $revIds
524
	 * @return array
525
	 */
526
	public static function getParentLengths( $db, array $revIds ) {
527
		$revLens = [];
528
		if ( !$revIds ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
529
			return $revLens; // empty
530
		}
531
		$res = $db->select( 'revision',
532
			[ 'rev_id', 'rev_len' ],
533
			[ 'rev_id' => $revIds ],
534
			__METHOD__ );
535
		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...
536
			$revLens[$row->rev_id] = $row->rev_len;
537
		}
538
		return $revLens;
539
	}
540
541
	/**
542
	 * Constructor
543
	 *
544
	 * @param object|array $row Either a database row or an array
545
	 * @throws MWException
546
	 * @access private
547
	 */
548
	function __construct( $row ) {
549
		if ( is_object( $row ) ) {
550
			$this->mId = intval( $row->rev_id );
551
			$this->mPage = intval( $row->rev_page );
552
			$this->mTextId = intval( $row->rev_text_id );
553
			$this->mComment = $row->rev_comment;
554
			$this->mUser = intval( $row->rev_user );
555
			$this->mMinorEdit = intval( $row->rev_minor_edit );
556
			$this->mTimestamp = $row->rev_timestamp;
557
			$this->mDeleted = intval( $row->rev_deleted );
558
559
			if ( !isset( $row->rev_parent_id ) ) {
560
				$this->mParentId = null;
561
			} else {
562
				$this->mParentId = intval( $row->rev_parent_id );
563
			}
564
565
			if ( !isset( $row->rev_len ) ) {
566
				$this->mSize = null;
567
			} else {
568
				$this->mSize = intval( $row->rev_len );
569
			}
570
571
			if ( !isset( $row->rev_sha1 ) ) {
572
				$this->mSha1 = null;
573
			} else {
574
				$this->mSha1 = $row->rev_sha1;
575
			}
576
577
			if ( isset( $row->page_latest ) ) {
578
				$this->mCurrent = ( $row->rev_id == $row->page_latest );
579
				$this->mTitle = Title::newFromRow( $row );
580
			} else {
581
				$this->mCurrent = false;
582
				$this->mTitle = null;
583
			}
584
585
			if ( !isset( $row->rev_content_model ) ) {
586
				$this->mContentModel = null; # determine on demand if needed
587
			} else {
588
				$this->mContentModel = strval( $row->rev_content_model );
589
			}
590
591
			if ( !isset( $row->rev_content_format ) ) {
592
				$this->mContentFormat = null; # determine on demand if needed
593
			} else {
594
				$this->mContentFormat = strval( $row->rev_content_format );
595
			}
596
597
			// Lazy extraction...
598
			$this->mText = null;
599
			if ( isset( $row->old_text ) ) {
600
				$this->mTextRow = $row;
601
			} else {
602
				// 'text' table row entry will be lazy-loaded
603
				$this->mTextRow = null;
604
			}
605
606
			// Use user_name for users and rev_user_text for IPs...
607
			$this->mUserText = null; // lazy load if left null
608
			if ( $this->mUser == 0 ) {
609
				$this->mUserText = $row->rev_user_text; // IP user
610
			} elseif ( isset( $row->user_name ) ) {
611
				$this->mUserText = $row->user_name; // logged-in user
612
			}
613
			$this->mOrigUserText = $row->rev_user_text;
614
		} elseif ( is_array( $row ) ) {
615
			// Build a new revision to be saved...
616
			global $wgUser; // ugh
617
618
			# if we have a content object, use it to set the model and type
619
			if ( !empty( $row['content'] ) ) {
620
				// @todo when is that set? test with external store setup! check out insertOn() [dk]
621
				if ( !empty( $row['text_id'] ) ) {
622
					throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
623
						"can't serialize content object" );
624
				}
625
626
				$row['content_model'] = $row['content']->getModel();
627
				# note: mContentFormat is initializes later accordingly
628
				# note: content is serialized later in this method!
629
				# also set text to null?
630
			}
631
632
			$this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
633
			$this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
634
			$this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
635
			$this->mUserText = isset( $row['user_text'] )
636
				? strval( $row['user_text'] ) : $wgUser->getName();
637
			$this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
638
			$this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
639
			$this->mTimestamp = isset( $row['timestamp'] )
640
				? strval( $row['timestamp'] ) : wfTimestampNow();
641
			$this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
642
			$this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
643
			$this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
644
			$this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
645
646
			$this->mContentModel = isset( $row['content_model'] )
647
				? strval( $row['content_model'] ) : null;
648
			$this->mContentFormat = isset( $row['content_format'] )
649
				? strval( $row['content_format'] ) : null;
650
651
			// Enforce spacing trimming on supplied text
652
			$this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
653
			$this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
654
			$this->mTextRow = null;
655
656
			$this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
657
658
			// if we have a Content object, override mText and mContentModel
659
			if ( !empty( $row['content'] ) ) {
660
				if ( !( $row['content'] instanceof Content ) ) {
661
					throw new MWException( '`content` field must contain a Content object.' );
662
				}
663
664
				$handler = $this->getContentHandler();
665
				$this->mContent = $row['content'];
666
667
				$this->mContentModel = $this->mContent->getModel();
668
				$this->mContentHandler = null;
669
670
				$this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
671
			} elseif ( $this->mText !== null ) {
672
				$handler = $this->getContentHandler();
673
				$this->mContent = $handler->unserializeContent( $this->mText );
674
			}
675
676
			// If we have a Title object, make sure it is consistent with mPage.
677
			if ( $this->mTitle && $this->mTitle->exists() ) {
678
				if ( $this->mPage === null ) {
679
					// if the page ID wasn't known, set it now
680
					$this->mPage = $this->mTitle->getArticleID();
681
				} elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
682
					// Got different page IDs. This may be legit (e.g. during undeletion),
683
					// but it seems worth mentioning it in the log.
684
					wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
685
						$this->mTitle->getArticleID() . " provided by the Title object." );
686
				}
687
			}
688
689
			$this->mCurrent = false;
690
691
			// If we still have no length, see it we have the text to figure it out
692
			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...
693
				$this->mSize = $this->mContent->getSize();
694
			}
695
696
			// Same for sha1
697
			if ( $this->mSha1 === null ) {
698
				$this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
699
			}
700
701
			// force lazy init
702
			$this->getContentModel();
703
			$this->getContentFormat();
704
		} else {
705
			throw new MWException( 'Revision constructor passed invalid row format.' );
706
		}
707
		$this->mUnpatrolled = null;
0 ignored issues
show
Bug introduced by
The property mUnpatrolled does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
708
	}
709
710
	/**
711
	 * Get revision ID
712
	 *
713
	 * @return int|null
714
	 */
715
	public function getId() {
716
		return $this->mId;
717
	}
718
719
	/**
720
	 * Set the revision ID
721
	 *
722
	 * @since 1.19
723
	 * @param int $id
724
	 */
725
	public function setId( $id ) {
726
		$this->mId = $id;
727
	}
728
729
	/**
730
	 * Get text row ID
731
	 *
732
	 * @return int|null
733
	 */
734
	public function getTextId() {
735
		return $this->mTextId;
736
	}
737
738
	/**
739
	 * Get parent revision ID (the original previous page revision)
740
	 *
741
	 * @return int|null
742
	 */
743
	public function getParentId() {
744
		return $this->mParentId;
745
	}
746
747
	/**
748
	 * Returns the length of the text in this revision, or null if unknown.
749
	 *
750
	 * @return int|null
751
	 */
752
	public function getSize() {
753
		return $this->mSize;
754
	}
755
756
	/**
757
	 * Returns the base36 sha1 of the text in this revision, or null if unknown.
758
	 *
759
	 * @return string|null
760
	 */
761
	public function getSha1() {
762
		return $this->mSha1;
763
	}
764
765
	/**
766
	 * Returns the title of the page associated with this entry or null.
767
	 *
768
	 * Will do a query, when title is not set and id is given.
769
	 *
770
	 * @return Title|null
771
	 */
772
	public function getTitle() {
773
		if ( $this->mTitle !== null ) {
774
			return $this->mTitle;
775
		}
776
		// rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
777
		if ( $this->mId !== null ) {
778
			$dbr = wfGetDB( DB_SLAVE );
779
			$row = $dbr->selectRow(
780
				[ 'page', 'revision' ],
781
				self::selectPageFields(),
782
				[ 'page_id=rev_page',
783
					'rev_id' => $this->mId ],
784
				__METHOD__ );
785
			if ( $row ) {
786
				$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 779 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...
787
			}
788
		}
789
790
		if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
791
			$this->mTitle = Title::newFromID( $this->mPage );
792
		}
793
794
		return $this->mTitle;
795
	}
796
797
	/**
798
	 * Set the title of the revision
799
	 *
800
	 * @param Title $title
801
	 */
802
	public function setTitle( $title ) {
803
		$this->mTitle = $title;
804
	}
805
806
	/**
807
	 * Get the page ID
808
	 *
809
	 * @return int|null
810
	 */
811
	public function getPage() {
812
		return $this->mPage;
813
	}
814
815
	/**
816
	 * Fetch revision's user id if it's available to the specified audience.
817
	 * If the specified audience does not have access to it, zero will be
818
	 * returned.
819
	 *
820
	 * @param int $audience One of:
821
	 *   Revision::FOR_PUBLIC       to be displayed to all users
822
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
823
	 *   Revision::RAW              get the ID regardless of permissions
824
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
825
	 *   to the $audience parameter
826
	 * @return int
827
	 */
828 View Code Duplication
	public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
829
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
830
			return 0;
831
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
832
			return 0;
833
		} else {
834
			return $this->mUser;
835
		}
836
	}
837
838
	/**
839
	 * Fetch revision's user id without regard for the current user's permissions
840
	 *
841
	 * @return string
842
	 * @deprecated since 1.25, use getUser( Revision::RAW )
843
	 */
844
	public function getRawUser() {
845
		wfDeprecated( __METHOD__, '1.25' );
846
		return $this->getUser( self::RAW );
847
	}
848
849
	/**
850
	 * Fetch revision's username if it's available to the specified audience.
851
	 * If the specified audience does not have access to the username, an
852
	 * empty string will be returned.
853
	 *
854
	 * @param int $audience One of:
855
	 *   Revision::FOR_PUBLIC       to be displayed to all users
856
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
857
	 *   Revision::RAW              get the text regardless of permissions
858
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
859
	 *   to the $audience parameter
860
	 * @return string
861
	 */
862
	public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
863
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
864
			return '';
865
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
866
			return '';
867
		} else {
868
			if ( $this->mUserText === null ) {
869
				$this->mUserText = User::whoIs( $this->mUser ); // load on demand
870
				if ( $this->mUserText === false ) {
871
					# This shouldn't happen, but it can if the wiki was recovered
872
					# via importing revs and there is no user table entry yet.
873
					$this->mUserText = $this->mOrigUserText;
874
				}
875
			}
876
			return $this->mUserText;
877
		}
878
	}
879
880
	/**
881
	 * Fetch revision's username without regard for view restrictions
882
	 *
883
	 * @return string
884
	 * @deprecated since 1.25, use getUserText( Revision::RAW )
885
	 */
886
	public function getRawUserText() {
887
		wfDeprecated( __METHOD__, '1.25' );
888
		return $this->getUserText( self::RAW );
889
	}
890
891
	/**
892
	 * Fetch revision comment if it's available to the specified audience.
893
	 * If the specified audience does not have access to the comment, an
894
	 * empty string will be returned.
895
	 *
896
	 * @param int $audience One of:
897
	 *   Revision::FOR_PUBLIC       to be displayed to all users
898
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
899
	 *   Revision::RAW              get the text regardless of permissions
900
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
901
	 *   to the $audience parameter
902
	 * @return string
903
	 */
904 View Code Duplication
	function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
905
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
906
			return '';
907
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
908
			return '';
909
		} else {
910
			return $this->mComment;
911
		}
912
	}
913
914
	/**
915
	 * Fetch revision comment without regard for the current user's permissions
916
	 *
917
	 * @return string
918
	 * @deprecated since 1.25, use getComment( Revision::RAW )
919
	 */
920
	public function getRawComment() {
921
		wfDeprecated( __METHOD__, '1.25' );
922
		return $this->getComment( self::RAW );
923
	}
924
925
	/**
926
	 * @return bool
927
	 */
928
	public function isMinor() {
929
		return (bool)$this->mMinorEdit;
930
	}
931
932
	/**
933
	 * @return int Rcid of the unpatrolled row, zero if there isn't one
934
	 */
935
	public function isUnpatrolled() {
936
		if ( $this->mUnpatrolled !== null ) {
937
			return $this->mUnpatrolled;
938
		}
939
		$rc = $this->getRecentChange();
940
		if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
941
			$this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
942
		} else {
943
			$this->mUnpatrolled = 0;
944
		}
945
		return $this->mUnpatrolled;
946
	}
947
948
	/**
949
	 * Get the RC object belonging to the current revision, if there's one
950
	 *
951
	 * @param int $flags (optional) $flags include:
952
	 *      Revision::READ_LATEST  : Select the data from the master
953
	 *
954
	 * @since 1.22
955
	 * @return RecentChange|null
956
	 */
957
	public function getRecentChange( $flags = 0 ) {
958
		$dbr = wfGetDB( DB_SLAVE );
959
960
		list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
961
962
		return RecentChange::newFromConds(
963
			[
964
				'rc_user_text' => $this->getUserText( Revision::RAW ),
965
				'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
966
				'rc_this_oldid' => $this->getId()
967
			],
968
			__METHOD__,
969
			$dbType
970
		);
971
	}
972
973
	/**
974
	 * @param int $field One of DELETED_* bitfield constants
975
	 *
976
	 * @return bool
977
	 */
978
	public function isDeleted( $field ) {
979
		return ( $this->mDeleted & $field ) == $field;
980
	}
981
982
	/**
983
	 * Get the deletion bitfield of the revision
984
	 *
985
	 * @return int
986
	 */
987
	public function getVisibility() {
988
		return (int)$this->mDeleted;
989
	}
990
991
	/**
992
	 * Fetch revision text if it's available to the specified audience.
993
	 * If the specified audience does not have the ability to view this
994
	 * revision, an empty string will be returned.
995
	 *
996
	 * @param int $audience One of:
997
	 *   Revision::FOR_PUBLIC       to be displayed to all users
998
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
999
	 *   Revision::RAW              get the text regardless of permissions
1000
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1001
	 *   to the $audience parameter
1002
	 *
1003
	 * @deprecated since 1.21, use getContent() instead
1004
	 * @todo Replace usage in core
1005
	 * @return string
1006
	 */
1007
	public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
1008
		ContentHandler::deprecated( __METHOD__, '1.21' );
1009
1010
		$content = $this->getContent( $audience, $user );
1011
		return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
1012
	}
1013
1014
	/**
1015
	 * Fetch revision content if it's available to the specified audience.
1016
	 * If the specified audience does not have the ability to view this
1017
	 * revision, null will be returned.
1018
	 *
1019
	 * @param int $audience One of:
1020
	 *   Revision::FOR_PUBLIC       to be displayed to all users
1021
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
1022
	 *   Revision::RAW              get the text regardless of permissions
1023
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1024
	 *   to the $audience parameter
1025
	 * @since 1.21
1026
	 * @return Content|null
1027
	 */
1028
	public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
1029
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
1030
			return null;
1031
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
1032
			return null;
1033
		} else {
1034
			return $this->getContentInternal();
1035
		}
1036
	}
1037
1038
	/**
1039
	 * Fetch revision text without regard for view restrictions
1040
	 *
1041
	 * @return string
1042
	 *
1043
	 * @deprecated since 1.21. Instead, use Revision::getContent( Revision::RAW )
1044
	 *                         or Revision::getSerializedData() as appropriate.
1045
	 */
1046
	public function getRawText() {
1047
		ContentHandler::deprecated( __METHOD__, "1.21" );
1048
		return $this->getText( self::RAW );
0 ignored issues
show
Deprecated Code introduced by
The method Revision::getText() has been deprecated with message: since 1.21, use getContent() instead

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

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

Loading history...
1049
	}
1050
1051
	/**
1052
	 * Fetch original serialized data without regard for view restrictions
1053
	 *
1054
	 * @since 1.21
1055
	 * @return string
1056
	 */
1057
	public function getSerializedData() {
1058
		if ( $this->mText === null ) {
1059
			$this->mText = $this->loadText();
1060
		}
1061
1062
		return $this->mText;
1063
	}
1064
1065
	/**
1066
	 * Gets the content object for the revision (or null on failure).
1067
	 *
1068
	 * Note that for mutable Content objects, each call to this method will return a
1069
	 * fresh clone.
1070
	 *
1071
	 * @since 1.21
1072
	 * @return Content|null The Revision's content, or null on failure.
1073
	 */
1074
	protected function getContentInternal() {
1075
		if ( $this->mContent === null ) {
1076
			// Revision is immutable. Load on demand:
1077
			if ( $this->mText === null ) {
1078
				$this->mText = $this->loadText();
1079
			}
1080
1081
			if ( $this->mText !== null && $this->mText !== false ) {
1082
				// Unserialize content
1083
				$handler = $this->getContentHandler();
1084
				$format = $this->getContentFormat();
1085
1086
				$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...
1087
			}
1088
		}
1089
1090
		// NOTE: copy() will return $this for immutable content objects
1091
		return $this->mContent ? $this->mContent->copy() : null;
1092
	}
1093
1094
	/**
1095
	 * Returns the content model for this revision.
1096
	 *
1097
	 * If no content model was stored in the database, the default content model for the title is
1098
	 * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
1099
	 * is used as a last resort.
1100
	 *
1101
	 * @return string The content model id associated with this revision,
1102
	 *     see the CONTENT_MODEL_XXX constants.
1103
	 **/
1104
	public function getContentModel() {
1105
		if ( !$this->mContentModel ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mContentModel of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1106
			$title = $this->getTitle();
1107
			if ( $title ) {
1108
				$this->mContentModel = ContentHandler::getDefaultModelFor( $title );
1109
			} else {
1110
				$this->mContentModel = CONTENT_MODEL_WIKITEXT;
1111
			}
1112
1113
			assert( !empty( $this->mContentModel ) );
1114
		}
1115
1116
		return $this->mContentModel;
1117
	}
1118
1119
	/**
1120
	 * Returns the content format for this revision.
1121
	 *
1122
	 * If no content format was stored in the database, the default format for this
1123
	 * revision's content model is returned.
1124
	 *
1125
	 * @return string The content format id associated with this revision,
1126
	 *     see the CONTENT_FORMAT_XXX constants.
1127
	 **/
1128
	public function getContentFormat() {
1129
		if ( !$this->mContentFormat ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mContentFormat of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

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