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

Revision::loadText()   D

Complexity

Conditions 16
Paths 260

Size

Total Lines 71
Code Lines 44

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 71
rs 4.1531
cc 16
eloc 44
nc 260
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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