Completed
Branch master (246348)
by
unknown
22:34
created

Revision::setUserIdAndName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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