Completed
Branch master (939199)
by
unknown
39:35
created

includes/Revision.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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