Completed
Branch master (62f6c6)
by
unknown
21:31
created

Revision::isUnpatrolled()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 3
nop 0
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * Representation of a page version.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
use MediaWiki\Linker\LinkTarget;
23
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
	 * @since 1.19
724
	 * @param int $id
725
	 */
726
	public function setId( $id ) {
727
		$this->mId = $id;
728
	}
729
730
	/**
731
	 * Get text row ID
732
	 *
733
	 * @return int|null
734
	 */
735
	public function getTextId() {
736
		return $this->mTextId;
737
	}
738
739
	/**
740
	 * Get parent revision ID (the original previous page revision)
741
	 *
742
	 * @return int|null
743
	 */
744
	public function getParentId() {
745
		return $this->mParentId;
746
	}
747
748
	/**
749
	 * Returns the length of the text in this revision, or null if unknown.
750
	 *
751
	 * @return int|null
752
	 */
753
	public function getSize() {
754
		return $this->mSize;
755
	}
756
757
	/**
758
	 * Returns the base36 sha1 of the text in this revision, or null if unknown.
759
	 *
760
	 * @return string|null
761
	 */
762
	public function getSha1() {
763
		return $this->mSha1;
764
	}
765
766
	/**
767
	 * Returns the title of the page associated with this entry or null.
768
	 *
769
	 * Will do a query, when title is not set and id is given.
770
	 *
771
	 * @return Title|null
772
	 */
773
	public function getTitle() {
774
		if ( $this->mTitle !== null ) {
775
			return $this->mTitle;
776
		}
777
		// rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
778
		if ( $this->mId !== null ) {
779
			$dbr = wfGetDB( DB_SLAVE );
780
			$row = $dbr->selectRow(
781
				[ 'page', 'revision' ],
782
				self::selectPageFields(),
783
				[ 'page_id=rev_page',
784
					'rev_id' => $this->mId ],
785
				__METHOD__ );
786
			if ( $row ) {
787
				$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 780 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...
788
			}
789
		}
790
791
		if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
792
			$this->mTitle = Title::newFromID( $this->mPage );
793
		}
794
795
		return $this->mTitle;
796
	}
797
798
	/**
799
	 * Set the title of the revision
800
	 *
801
	 * @param Title $title
802
	 */
803
	public function setTitle( $title ) {
804
		$this->mTitle = $title;
805
	}
806
807
	/**
808
	 * Get the page ID
809
	 *
810
	 * @return int|null
811
	 */
812
	public function getPage() {
813
		return $this->mPage;
814
	}
815
816
	/**
817
	 * Fetch revision's user id if it's available to the specified audience.
818
	 * If the specified audience does not have access to it, zero will be
819
	 * returned.
820
	 *
821
	 * @param int $audience One of:
822
	 *   Revision::FOR_PUBLIC       to be displayed to all users
823
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
824
	 *   Revision::RAW              get the ID regardless of permissions
825
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
826
	 *   to the $audience parameter
827
	 * @return int
828
	 */
829 View Code Duplication
	public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
830
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
831
			return 0;
832
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
833
			return 0;
834
		} else {
835
			return $this->mUser;
836
		}
837
	}
838
839
	/**
840
	 * Fetch revision's user id without regard for the current user's permissions
841
	 *
842
	 * @return string
843
	 * @deprecated since 1.25, use getUser( Revision::RAW )
844
	 */
845
	public function getRawUser() {
846
		wfDeprecated( __METHOD__, '1.25' );
847
		return $this->getUser( self::RAW );
848
	}
849
850
	/**
851
	 * Fetch revision's username if it's available to the specified audience.
852
	 * If the specified audience does not have access to the username, an
853
	 * empty string will be returned.
854
	 *
855
	 * @param int $audience One of:
856
	 *   Revision::FOR_PUBLIC       to be displayed to all users
857
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
858
	 *   Revision::RAW              get the text regardless of permissions
859
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
860
	 *   to the $audience parameter
861
	 * @return string
862
	 */
863
	public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
864
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
865
			return '';
866
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
867
			return '';
868
		} else {
869
			if ( $this->mUserText === null ) {
870
				$this->mUserText = User::whoIs( $this->mUser ); // load on demand
871
				if ( $this->mUserText === false ) {
872
					# This shouldn't happen, but it can if the wiki was recovered
873
					# via importing revs and there is no user table entry yet.
874
					$this->mUserText = $this->mOrigUserText;
875
				}
876
			}
877
			return $this->mUserText;
878
		}
879
	}
880
881
	/**
882
	 * Fetch revision's username without regard for view restrictions
883
	 *
884
	 * @return string
885
	 * @deprecated since 1.25, use getUserText( Revision::RAW )
886
	 */
887
	public function getRawUserText() {
888
		wfDeprecated( __METHOD__, '1.25' );
889
		return $this->getUserText( self::RAW );
890
	}
891
892
	/**
893
	 * Fetch revision comment if it's available to the specified audience.
894
	 * If the specified audience does not have access to the comment, an
895
	 * empty string will be returned.
896
	 *
897
	 * @param int $audience One of:
898
	 *   Revision::FOR_PUBLIC       to be displayed to all users
899
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
900
	 *   Revision::RAW              get the text regardless of permissions
901
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
902
	 *   to the $audience parameter
903
	 * @return string
904
	 */
905 View Code Duplication
	function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
906
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
907
			return '';
908
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
909
			return '';
910
		} else {
911
			return $this->mComment;
912
		}
913
	}
914
915
	/**
916
	 * Fetch revision comment without regard for the current user's permissions
917
	 *
918
	 * @return string
919
	 * @deprecated since 1.25, use getComment( Revision::RAW )
920
	 */
921
	public function getRawComment() {
922
		wfDeprecated( __METHOD__, '1.25' );
923
		return $this->getComment( self::RAW );
924
	}
925
926
	/**
927
	 * @return bool
928
	 */
929
	public function isMinor() {
930
		return (bool)$this->mMinorEdit;
931
	}
932
933
	/**
934
	 * @return int Rcid of the unpatrolled row, zero if there isn't one
935
	 */
936
	public function isUnpatrolled() {
937
		if ( $this->mUnpatrolled !== null ) {
938
			return $this->mUnpatrolled;
939
		}
940
		$rc = $this->getRecentChange();
941
		if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
942
			$this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
943
		} else {
944
			$this->mUnpatrolled = 0;
945
		}
946
		return $this->mUnpatrolled;
947
	}
948
949
	/**
950
	 * Get the RC object belonging to the current revision, if there's one
951
	 *
952
	 * @param int $flags (optional) $flags include:
953
	 *      Revision::READ_LATEST  : Select the data from the master
954
	 *
955
	 * @since 1.22
956
	 * @return RecentChange|null
957
	 */
958
	public function getRecentChange( $flags = 0 ) {
959
		$dbr = wfGetDB( DB_SLAVE );
960
961
		list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
962
963
		return RecentChange::newFromConds(
964
			[
965
				'rc_user_text' => $this->getUserText( Revision::RAW ),
966
				'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
967
				'rc_this_oldid' => $this->getId()
968
			],
969
			__METHOD__,
970
			$dbType
971
		);
972
	}
973
974
	/**
975
	 * @param int $field One of DELETED_* bitfield constants
976
	 *
977
	 * @return bool
978
	 */
979
	public function isDeleted( $field ) {
980
		return ( $this->mDeleted & $field ) == $field;
981
	}
982
983
	/**
984
	 * Get the deletion bitfield of the revision
985
	 *
986
	 * @return int
987
	 */
988
	public function getVisibility() {
989
		return (int)$this->mDeleted;
990
	}
991
992
	/**
993
	 * Fetch revision text if it's available to the specified audience.
994
	 * If the specified audience does not have the ability to view this
995
	 * revision, an empty string will be returned.
996
	 *
997
	 * @param int $audience One of:
998
	 *   Revision::FOR_PUBLIC       to be displayed to all users
999
	 *   Revision::FOR_THIS_USER    to be displayed to the given user
1000
	 *   Revision::RAW              get the text regardless of permissions
1001
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1002
	 *   to the $audience parameter
1003
	 *
1004
	 * @deprecated since 1.21, use getContent() instead
1005
	 * @todo Replace usage in core
1006
	 * @return string
1007
	 */
1008
	public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
1009
		ContentHandler::deprecated( __METHOD__, '1.21' );
1010
1011
		$content = $this->getContent( $audience, $user );
1012
		return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
1013
	}
1014
1015
	/**
1016
	 * Fetch revision content if it's available to the specified audience.
1017
	 * If the specified audience does not have the ability to view this
1018
	 * revision, null will be returned.
1019
	 *
1020
	 * @param int $audience One of:
1021
	 *   Revision::FOR_PUBLIC       to be displayed to all users
1022
	 *   Revision::FOR_THIS_USER    to be displayed to $wgUser
1023
	 *   Revision::RAW              get the text regardless of permissions
1024
	 * @param User $user User object to check for, only if FOR_THIS_USER is passed
1025
	 *   to the $audience parameter
1026
	 * @since 1.21
1027
	 * @return Content|null
1028
	 */
1029
	public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
1030
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
1031
			return null;
1032
		} elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
1033
			return null;
1034
		} else {
1035
			return $this->getContentInternal();
1036
		}
1037
	}
1038
1039
	/**
1040
	 * Fetch original serialized data without regard for view restrictions
1041
	 *
1042
	 * @since 1.21
1043
	 * @return string
1044
	 */
1045
	public function getSerializedData() {
1046
		if ( $this->mText === null ) {
1047
			$this->mText = $this->loadText();
1048
		}
1049
1050
		return $this->mText;
1051
	}
1052
1053
	/**
1054
	 * Gets the content object for the revision (or null on failure).
1055
	 *
1056
	 * Note that for mutable Content objects, each call to this method will return a
1057
	 * fresh clone.
1058
	 *
1059
	 * @since 1.21
1060
	 * @return Content|null The Revision's content, or null on failure.
1061
	 */
1062
	protected function getContentInternal() {
1063
		if ( $this->mContent === null ) {
1064
			// Revision is immutable. Load on demand:
1065
			if ( $this->mText === null ) {
1066
				$this->mText = $this->loadText();
1067
			}
1068
1069
			if ( $this->mText !== null && $this->mText !== false ) {
1070
				// Unserialize content
1071
				$handler = $this->getContentHandler();
1072
				$format = $this->getContentFormat();
1073
1074
				$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...
1075
			}
1076
		}
1077
1078
		// NOTE: copy() will return $this for immutable content objects
1079
		return $this->mContent ? $this->mContent->copy() : null;
1080
	}
1081
1082
	/**
1083
	 * Returns the content model for this revision.
1084
	 *
1085
	 * If no content model was stored in the database, the default content model for the title is
1086
	 * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
1087
	 * is used as a last resort.
1088
	 *
1089
	 * @return string The content model id associated with this revision,
1090
	 *     see the CONTENT_MODEL_XXX constants.
1091
	 **/
1092
	public function getContentModel() {
1093
		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...
1094
			$title = $this->getTitle();
1095
			if ( $title ) {
1096
				$this->mContentModel = ContentHandler::getDefaultModelFor( $title );
1097
			} else {
1098
				$this->mContentModel = CONTENT_MODEL_WIKITEXT;
1099
			}
1100
1101
			assert( !empty( $this->mContentModel ) );
1102
		}
1103
1104
		return $this->mContentModel;
1105
	}
1106
1107
	/**
1108
	 * Returns the content format for this revision.
1109
	 *
1110
	 * If no content format was stored in the database, the default format for this
1111
	 * revision's content model is returned.
1112
	 *
1113
	 * @return string The content format id associated with this revision,
1114
	 *     see the CONTENT_FORMAT_XXX constants.
1115
	 **/
1116
	public function getContentFormat() {
1117
		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...
1118
			$handler = $this->getContentHandler();
1119
			$this->mContentFormat = $handler->getDefaultFormat();
1120
1121
			assert( !empty( $this->mContentFormat ) );
1122
		}
1123
1124
		return $this->mContentFormat;
1125
	}
1126
1127
	/**
1128
	 * Returns the content handler appropriate for this revision's content model.
1129
	 *
1130
	 * @throws MWException
1131
	 * @return ContentHandler
1132
	 */
1133
	public function getContentHandler() {
1134
		if ( !$this->mContentHandler ) {
1135
			$model = $this->getContentModel();
1136
			$this->mContentHandler = ContentHandler::getForModelID( $model );
1137
1138
			$format = $this->getContentFormat();
1139
1140
			if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
1141
				throw new MWException( "Oops, the content format $format is not supported for "
1142
					. "this content model, $model" );
1143
			}
1144
		}
1145
1146
		return $this->mContentHandler;
1147
	}
1148
1149
	/**
1150
	 * @return string
1151
	 */
1152
	public function getTimestamp() {
1153
		return wfTimestamp( TS_MW, $this->mTimestamp );
1154
	}
1155
1156
	/**
1157
	 * @return bool
1158
	 */
1159
	public function isCurrent() {
1160
		return $this->mCurrent;
1161
	}
1162
1163
	/**
1164
	 * Get previous revision for this title
1165
	 *
1166
	 * @return Revision|null
1167
	 */
1168 View Code Duplication
	public function getPrevious() {
1169
		if ( $this->getTitle() ) {
1170
			$prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
1171
			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...
1172
				return self::newFromTitle( $this->getTitle(), $prev );
1173
			}
1174
		}
1175
		return null;
1176
	}
1177
1178
	/**
1179
	 * Get next revision for this title
1180
	 *
1181
	 * @return Revision|null
1182
	 */
1183 View Code Duplication
	public function getNext() {
1184
		if ( $this->getTitle() ) {
1185
			$next = $this->getTitle()->getNextRevisionID( $this->getId() );
1186
			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...
1187
				return self::newFromTitle( $this->getTitle(), $next );
1188
			}
1189
		}
1190
		return null;
1191
	}
1192
1193
	/**
1194
	 * Get previous revision Id for this page_id
1195
	 * This is used to populate rev_parent_id on save
1196
	 *
1197
	 * @param IDatabase $db
1198
	 * @return int
1199
	 */
1200
	private function getPreviousRevisionId( $db ) {
1201
		if ( $this->mPage === null ) {
1202
			return 0;
1203
		}
1204
		# Use page_latest if ID is not given
1205
		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...
1206
			$prevId = $db->selectField( 'page', 'page_latest',
1207
				[ 'page_id' => $this->mPage ],
1208
				__METHOD__ );
1209
		} else {
1210
			$prevId = $db->selectField( 'revision', 'rev_id',
1211
				[ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
1212
				__METHOD__,
1213
				[ 'ORDER BY' => 'rev_id DESC' ] );
1214
		}
1215
		return intval( $prevId );
1216
	}
1217
1218
	/**
1219
	 * Get revision text associated with an old or archive row
1220
	 * $row is usually an object from wfFetchRow(), both the flags and the text
1221
	 * field must be included.
1222
	 *
1223
	 * @param stdClass $row The text data
1224
	 * @param string $prefix Table prefix (default 'old_')
1225
	 * @param string|bool $wiki The name of the wiki to load the revision text from
1226
	 *   (same as the the wiki $row was loaded from) or false to indicate the local
1227
	 *   wiki (this is the default). Otherwise, it must be a symbolic wiki database
1228
	 *   identifier as understood by the LoadBalancer class.
1229
	 * @return string Text the text requested or false on failure
1230
	 */
1231
	public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
1232
1233
		# Get data
1234
		$textField = $prefix . 'text';
1235
		$flagsField = $prefix . 'flags';
1236
1237
		if ( isset( $row->$flagsField ) ) {
1238
			$flags = explode( ',', $row->$flagsField );
1239
		} else {
1240
			$flags = [];
1241
		}
1242
1243
		if ( isset( $row->$textField ) ) {
1244
			$text = $row->$textField;
1245
		} else {
1246
			return false;
1247
		}
1248
1249
		# Use external methods for external objects, text in table is URL-only then
1250
		if ( in_array( 'external', $flags ) ) {
1251
			$url = $text;
1252
			$parts = explode( '://', $url, 2 );
1253
			if ( count( $parts ) == 1 || $parts[1] == '' ) {
1254
				return false;
1255
			}
1256
			$text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
1257
		}
1258
1259
		// If the text was fetched without an error, convert it
1260
		if ( $text !== false ) {
1261
			$text = self::decompressRevisionText( $text, $flags );
1262
		}
1263
		return $text;
1264
	}
1265
1266
	/**
1267
	 * If $wgCompressRevisions is enabled, we will compress data.
1268
	 * The input string is modified in place.
1269
	 * Return value is the flags field: contains 'gzip' if the
1270
	 * data is compressed, and 'utf-8' if we're saving in UTF-8
1271
	 * mode.
1272
	 *
1273
	 * @param mixed $text Reference to a text
1274
	 * @return string
1275
	 */
1276
	public static function compressRevisionText( &$text ) {
1277
		global $wgCompressRevisions;
1278
		$flags = [];
1279
1280
		# Revisions not marked this way will be converted
1281
		# on load if $wgLegacyCharset is set in the future.
1282
		$flags[] = 'utf-8';
1283
1284
		if ( $wgCompressRevisions ) {
1285
			if ( function_exists( 'gzdeflate' ) ) {
1286
				$deflated = gzdeflate( $text );
1287
1288
				if ( $deflated === false ) {
1289
					wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
1290
				} else {
1291
					$text = $deflated;
1292
					$flags[] = 'gzip';
1293
				}
1294
			} else {
1295
				wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
1296
			}
1297
		}
1298
		return implode( ',', $flags );
1299
	}
1300
1301
	/**
1302
	 * Re-converts revision text according to it's flags.
1303
	 *
1304
	 * @param mixed $text Reference to a text
1305
	 * @param array $flags Compression flags
1306
	 * @return string|bool Decompressed text, or false on failure
1307
	 */
1308
	public static function decompressRevisionText( $text, $flags ) {
1309
		if ( in_array( 'gzip', $flags ) ) {
1310
			# Deal with optional compression of archived pages.
1311
			# This can be done periodically via maintenance/compressOld.php, and
1312
			# as pages are saved if $wgCompressRevisions is set.
1313
			$text = gzinflate( $text );
1314
1315
			if ( $text === false ) {
1316
				wfLogWarning( __METHOD__ . ': gzinflate() failed' );
1317
				return false;
1318
			}
1319
		}
1320
1321
		if ( in_array( 'object', $flags ) ) {
1322
			# Generic compressed storage
1323
			$obj = unserialize( $text );
1324
			if ( !is_object( $obj ) ) {
1325
				// Invalid object
1326
				return false;
1327
			}
1328
			$text = $obj->getText();
1329
		}
1330
1331
		global $wgLegacyEncoding;
1332
		if ( $text !== false && $wgLegacyEncoding
1333
			&& !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
1334
		) {
1335
			# Old revisions kept around in a legacy encoding?
1336
			# Upconvert on demand.
1337
			# ("utf8" checked for compatibility with some broken
1338
			#  conversion scripts 2008-12-30)
1339
			global $wgContLang;
1340
			$text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
1341
		}
1342
1343
		return $text;
1344
	}
1345
1346
	/**
1347
	 * Insert a new revision into the database, returning the new revision ID
1348
	 * number on success and dies horribly on failure.
1349
	 *
1350
	 * @param IDatabase $dbw (master connection)
1351
	 * @throws MWException
1352
	 * @return int
1353
	 */
1354
	public function insertOn( $dbw ) {
1355
		global $wgDefaultExternalStore, $wgContentHandlerUseDB;
1356
1357
		// Not allowed to have rev_page equal to 0, false, etc.
1358 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...
1359
			$title = $this->getTitle();
1360
			if ( $title instanceof Title ) {
1361
				$titleText = ' for page ' . $title->getPrefixedText();
1362
			} else {
1363
				$titleText = '';
1364
			}
1365
			throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
1366
		}
1367
1368
		$this->checkContentModel();
1369
1370
		$data = $this->mText;
1371
		$flags = self::compressRevisionText( $data );
1372
1373
		# Write to external storage if required
1374
		if ( $wgDefaultExternalStore ) {
1375
			// Store and get the URL
1376
			$data = ExternalStore::insertToDefault( $data );
1377
			if ( !$data ) {
1378
				throw new MWException( "Unable to store text to external storage" );
1379
			}
1380
			if ( $flags ) {
1381
				$flags .= ',';
1382
			}
1383
			$flags .= 'external';
1384
		}
1385
1386
		# Record the text (or external storage URL) to the text table
1387
		if ( $this->mTextId === null ) {
1388
			$old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
1389
			$dbw->insert( 'text',
1390
				[
1391
					'old_id' => $old_id,
1392
					'old_text' => $data,
1393
					'old_flags' => $flags,
1394
				], __METHOD__
1395
			);
1396
			$this->mTextId = $dbw->insertId();
1397
		}
1398
1399
		if ( $this->mComment === null ) {
1400
			$this->mComment = "";
1401
		}
1402
1403
		# Record the edit in revisions
1404
		$rev_id = $this->mId !== null
1405
			? $this->mId
1406
			: $dbw->nextSequenceValue( 'revision_rev_id_seq' );
1407
		$row = [
1408
			'rev_id'         => $rev_id,
1409
			'rev_page'       => $this->mPage,
1410
			'rev_text_id'    => $this->mTextId,
1411
			'rev_comment'    => $this->mComment,
1412
			'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
1413
			'rev_user'       => $this->mUser,
1414
			'rev_user_text'  => $this->mUserText,
1415
			'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
1416
			'rev_deleted'    => $this->mDeleted,
1417
			'rev_len'        => $this->mSize,
1418
			'rev_parent_id'  => $this->mParentId === null
1419
				? $this->getPreviousRevisionId( $dbw )
1420
				: $this->mParentId,
1421
			'rev_sha1'       => $this->mSha1 === null
1422
				? 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...
1423
				: $this->mSha1,
1424
		];
1425
1426
		if ( $wgContentHandlerUseDB ) {
1427
			// NOTE: Store null for the default model and format, to save space.
1428
			// XXX: Makes the DB sensitive to changed defaults.
1429
			// Make this behavior optional? Only in miser mode?
1430
1431
			$model = $this->getContentModel();
1432
			$format = $this->getContentFormat();
1433
1434
			$title = $this->getTitle();
1435
1436
			if ( $title === null ) {
1437
				throw new MWException( "Insufficient information to determine the title of the "
1438
					. "revision's page!" );
1439
			}
1440
1441
			$defaultModel = ContentHandler::getDefaultModelFor( $title );
1442
			$defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
1443
1444
			$row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
1445
			$row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
1446
		}
1447
1448
		$dbw->insert( 'revision', $row, __METHOD__ );
1449
1450
		$this->mId = $rev_id !== null ? $rev_id : $dbw->insertId();
1451
1452
		// Assertion to try to catch T92046
1453
		if ( (int)$this->mId === 0 ) {
1454
			throw new UnexpectedValueException(
1455
				'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
1456
					var_export( $row, 1 )
1457
			);
1458
		}
1459
1460
		Hooks::run( 'RevisionInsertComplete', [ &$this, $data, $flags ] );
1461
1462
		return $this->mId;
1463
	}
1464
1465
	protected function checkContentModel() {
1466
		global $wgContentHandlerUseDB;
1467
1468
		// Note: may return null for revisions that have not yet been inserted
1469
		$title = $this->getTitle();
1470
1471
		$model = $this->getContentModel();
1472
		$format = $this->getContentFormat();
1473
		$handler = $this->getContentHandler();
1474
1475
		if ( !$handler->isSupportedFormat( $format ) ) {
1476
			$t = $title->getPrefixedDBkey();
1477
1478
			throw new MWException( "Can't use format $format with content model $model on $t" );
1479
		}
1480
1481
		if ( !$wgContentHandlerUseDB && $title ) {
1482
			// if $wgContentHandlerUseDB is not set,
1483
			// all revisions must use the default content model and format.
1484
1485
			$defaultModel = ContentHandler::getDefaultModelFor( $title );
1486
			$defaultHandler = ContentHandler::getForModelID( $defaultModel );
1487
			$defaultFormat = $defaultHandler->getDefaultFormat();
1488
1489
			if ( $this->getContentModel() != $defaultModel ) {
1490
				$t = $title->getPrefixedDBkey();
1491
1492
				throw new MWException( "Can't save non-default content model with "
1493
					. "\$wgContentHandlerUseDB disabled: model is $model, "
1494
					. "default for $t is $defaultModel" );
1495
			}
1496
1497
			if ( $this->getContentFormat() != $defaultFormat ) {
1498
				$t = $title->getPrefixedDBkey();
1499
1500
				throw new MWException( "Can't use non-default content format with "
1501
					. "\$wgContentHandlerUseDB disabled: format is $format, "
1502
					. "default for $t is $defaultFormat" );
1503
			}
1504
		}
1505
1506
		$content = $this->getContent( Revision::RAW );
1507
		$prefixedDBkey = $title->getPrefixedDBkey();
1508
		$revId = $this->mId;
1509
1510
		if ( !$content ) {
1511
			throw new MWException(
1512
				"Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
1513
			);
1514
		}
1515
		if ( !$content->isValid() ) {
1516
			throw new MWException(
1517
				"Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
1518
			);
1519
		}
1520
	}
1521
1522
	/**
1523
	 * Get the base 36 SHA-1 value for a string of text
1524
	 * @param string $text
1525
	 * @return string
1526
	 */
1527
	public static function base36Sha1( $text ) {
1528
		return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
1529
	}
1530
1531
	/**
1532
	 * Lazy-load the revision's text.
1533
	 * Currently hardcoded to the 'text' table storage engine.
1534
	 *
1535
	 * @return string|bool The revision's text, or false on failure
1536
	 */
1537
	protected function loadText() {
1538
		// Caching may be beneficial for massive use of external storage
1539
		global $wgRevisionCacheExpiry;
1540
		static $processCache = null;
1541
1542
		if ( !$processCache ) {
1543
			$processCache = new MapCacheLRU( 10 );
1544
		}
1545
1546
		$cache = ObjectCache::getMainWANInstance();
1547
		$textId = $this->getTextId();
1548
		$key = wfMemcKey( 'revisiontext', 'textid', $textId );
1549
1550
		if ( $wgRevisionCacheExpiry ) {
1551
			if ( $processCache->has( $key ) ) {
1552
				return $processCache->get( $key );
1553
			}
1554
			$text = $cache->get( $key );
1555
			if ( is_string( $text ) ) {
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 ) ) {
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...
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