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

SpecialUndelete::doesWrites()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 3
rs 10
c 1
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Implements Special:Undelete
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
 * @ingroup SpecialPage
22
 */
23
24
/**
25
 * Used to show archived pages and eventually restore them.
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class PageArchive {
30
	/** @var Title */
31
	protected $title;
32
33
	/** @var Status */
34
	protected $fileStatus;
35
36
	/** @var Status */
37
	protected $revisionStatus;
38
39
	/** @var Config */
40
	protected $config;
41
42
	function __construct( $title, Config $config = null ) {
43
		if ( is_null( $title ) ) {
44
			throw new MWException( __METHOD__ . ' given a null title.' );
45
		}
46
		$this->title = $title;
47 View Code Duplication
		if ( $config === null ) {
48
			wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
49
			$config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
50
		}
51
		$this->config = $config;
52
	}
53
54
	public function doesWrites() {
55
		return true;
56
	}
57
58
	/**
59
	 * List all deleted pages recorded in the archive table. Returns result
60
	 * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
61
	 * namespace/title.
62
	 *
63
	 * @return ResultWrapper
64
	 */
65
	public static function listAllPages() {
66
		$dbr = wfGetDB( DB_SLAVE );
67
68
		return self::listPages( $dbr, '' );
69
	}
70
71
	/**
72
	 * List deleted pages recorded in the archive table matching the
73
	 * given title prefix.
74
	 * Returns result wrapper with (ar_namespace, ar_title, count) fields.
75
	 *
76
	 * @param string $prefix Title prefix
77
	 * @return ResultWrapper
78
	 */
79
	public static function listPagesByPrefix( $prefix ) {
80
		$dbr = wfGetDB( DB_SLAVE );
81
82
		$title = Title::newFromText( $prefix );
83
		if ( $title ) {
84
			$ns = $title->getNamespace();
85
			$prefix = $title->getDBkey();
86
		} else {
87
			// Prolly won't work too good
88
			// @todo handle bare namespace names cleanly?
89
			$ns = 0;
90
		}
91
92
		$conds = [
93
			'ar_namespace' => $ns,
94
			'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
95
		];
96
97
		return self::listPages( $dbr, $conds );
98
	}
99
100
	/**
101
	 * @param IDatabase $dbr
102
	 * @param string|array $condition
103
	 * @return bool|ResultWrapper
104
	 */
105
	protected static function listPages( $dbr, $condition ) {
106
		return $dbr->select(
107
			[ 'archive' ],
108
			[
109
				'ar_namespace',
110
				'ar_title',
111
				'count' => 'COUNT(*)'
112
			],
113
			$condition,
114
			__METHOD__,
115
			[
116
				'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
117
				'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
118
				'LIMIT' => 100,
119
			]
120
		);
121
	}
122
123
	/**
124
	 * List the revisions of the given page. Returns result wrapper with
125
	 * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
126
	 *
127
	 * @return ResultWrapper
128
	 */
129
	function listRevisions() {
130
		$dbr = wfGetDB( DB_SLAVE );
131
132
		$tables = [ 'archive' ];
133
134
		$fields = [
135
			'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
136
			'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
137
		];
138
139
		if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
140
			$fields[] = 'ar_content_format';
141
			$fields[] = 'ar_content_model';
142
		}
143
144
		$conds = [ 'ar_namespace' => $this->title->getNamespace(),
145
			'ar_title' => $this->title->getDBkey() ];
146
147
		$options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
148
149
		$join_conds = [];
150
151
		ChangeTags::modifyDisplayQuery(
152
			$tables,
153
			$fields,
154
			$conds,
155
			$join_conds,
156
			$options,
157
			''
158
		);
159
160
		return $dbr->select( $tables,
161
			$fields,
162
			$conds,
0 ignored issues
show
Bug introduced by
It seems like $conds defined by array('ar_namespace' => ...his->title->getDBkey()) on line 144 can also be of type array; however, DatabaseBase::select() 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...
163
			__METHOD__,
164
			$options,
165
			$join_conds
166
		);
167
	}
168
169
	/**
170
	 * List the deleted file revisions for this page, if it's a file page.
171
	 * Returns a result wrapper with various filearchive fields, or null
172
	 * if not a file page.
173
	 *
174
	 * @return ResultWrapper
175
	 * @todo Does this belong in Image for fuller encapsulation?
176
	 */
177
	function listFiles() {
178
		if ( $this->title->getNamespace() != NS_FILE ) {
179
			return null;
180
		}
181
182
		$dbr = wfGetDB( DB_SLAVE );
183
		return $dbr->select(
184
			'filearchive',
185
			ArchivedFile::selectFields(),
186
			[ 'fa_name' => $this->title->getDBkey() ],
187
			__METHOD__,
188
			[ 'ORDER BY' => 'fa_timestamp DESC' ]
189
		);
190
	}
191
192
	/**
193
	 * Return a Revision object containing data for the deleted revision.
194
	 * Note that the result *may* or *may not* have a null page ID.
195
	 *
196
	 * @param string $timestamp
197
	 * @return Revision|null
198
	 */
199
	function getRevision( $timestamp ) {
200
		$dbr = wfGetDB( DB_SLAVE );
201
202
		$fields = [
203
			'ar_rev_id',
204
			'ar_text',
205
			'ar_comment',
206
			'ar_user',
207
			'ar_user_text',
208
			'ar_timestamp',
209
			'ar_minor_edit',
210
			'ar_flags',
211
			'ar_text_id',
212
			'ar_deleted',
213
			'ar_len',
214
			'ar_sha1',
215
		];
216
217
		if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
218
			$fields[] = 'ar_content_format';
219
			$fields[] = 'ar_content_model';
220
		}
221
222
		$row = $dbr->selectRow( 'archive',
223
			$fields,
224
			[ 'ar_namespace' => $this->title->getNamespace(),
225
				'ar_title' => $this->title->getDBkey(),
226
				'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
227
			__METHOD__ );
228
229
		if ( $row ) {
230
			return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('archive...imestamp)), __METHOD__) on line 222 can also be of type boolean; however, Revision::newFromArchiveRow() does only seem to accept object, 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...
231
		}
232
233
		return null;
234
	}
235
236
	/**
237
	 * Return the most-previous revision, either live or deleted, against
238
	 * the deleted revision given by timestamp.
239
	 *
240
	 * May produce unexpected results in case of history merges or other
241
	 * unusual time issues.
242
	 *
243
	 * @param string $timestamp
244
	 * @return Revision|null Null when there is no previous revision
245
	 */
246
	function getPreviousRevision( $timestamp ) {
247
		$dbr = wfGetDB( DB_SLAVE );
248
249
		// Check the previous deleted revision...
250
		$row = $dbr->selectRow( 'archive',
251
			'ar_timestamp',
252
			[ 'ar_namespace' => $this->title->getNamespace(),
253
				'ar_title' => $this->title->getDBkey(),
254
				'ar_timestamp < ' .
255
					$dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($timestamp) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
256
			__METHOD__,
257
			[
258
				'ORDER BY' => 'ar_timestamp DESC',
259
				'LIMIT' => 1 ] );
260
		$prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
261
262
		$row = $dbr->selectRow( [ 'page', 'revision' ],
263
			[ 'rev_id', 'rev_timestamp' ],
264
			[
265
				'page_namespace' => $this->title->getNamespace(),
266
				'page_title' => $this->title->getDBkey(),
267
				'page_id = rev_page',
268
				'rev_timestamp < ' .
269
					$dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($timestamp) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
270
			__METHOD__,
271
			[
272
				'ORDER BY' => 'rev_timestamp DESC',
273
				'LIMIT' => 1 ] );
274
		$prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
275
		$prevLiveId = $row ? intval( $row->rev_id ) : null;
276
277
		if ( $prevLive && $prevLive > $prevDeleted ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prevLive of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
278
			// Most prior revision was live
279
			return Revision::newFromId( $prevLiveId );
280
		} elseif ( $prevDeleted ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prevDeleted of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
281
			// Most prior revision was deleted
282
			return $this->getRevision( $prevDeleted );
283
		}
284
285
		// No prior revision on this page.
286
		return null;
287
	}
288
289
	/**
290
	 * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
291
	 *
292
	 * @param object $row Database row
293
	 * @return string
294
	 */
295
	function getTextFromRow( $row ) {
296
		if ( is_null( $row->ar_text_id ) ) {
297
			// An old row from MediaWiki 1.4 or previous.
298
			// Text is embedded in this row in classic compression format.
299
			return Revision::getRevisionText( $row, 'ar_' );
300
		}
301
302
		// New-style: keyed to the text storage backend.
303
		$dbr = wfGetDB( DB_SLAVE );
304
		$text = $dbr->selectRow( 'text',
305
			[ 'old_text', 'old_flags' ],
306
			[ 'old_id' => $row->ar_text_id ],
307
			__METHOD__ );
308
309
		return Revision::getRevisionText( $text );
0 ignored issues
show
Bug introduced by
It seems like $text defined by $dbr->selectRow('text', ...r_text_id), __METHOD__) on line 304 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...
310
	}
311
312
	/**
313
	 * Fetch (and decompress if necessary) the stored text of the most
314
	 * recently edited deleted revision of the page.
315
	 *
316
	 * If there are no archived revisions for the page, returns NULL.
317
	 *
318
	 * @return string|null
319
	 */
320
	function getLastRevisionText() {
321
		$dbr = wfGetDB( DB_SLAVE );
322
		$row = $dbr->selectRow( 'archive',
323
			[ 'ar_text', 'ar_flags', 'ar_text_id' ],
324
			[ 'ar_namespace' => $this->title->getNamespace(),
325
				'ar_title' => $this->title->getDBkey() ],
326
			__METHOD__,
327
			[ 'ORDER BY' => 'ar_timestamp DESC' ] );
328
329
		if ( $row ) {
330
			return $this->getTextFromRow( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('archive...> 'ar_timestamp DESC')) on line 322 can also be of type boolean; however, PageArchive::getTextFromRow() does only seem to accept object, 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...
331
		}
332
333
		return null;
334
	}
335
336
	/**
337
	 * Quick check if any archived revisions are present for the page.
338
	 *
339
	 * @return bool
340
	 */
341
	function isDeleted() {
342
		$dbr = wfGetDB( DB_SLAVE );
343
		$n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
344
			[ 'ar_namespace' => $this->title->getNamespace(),
345
				'ar_title' => $this->title->getDBkey() ],
346
			__METHOD__
347
		);
348
349
		return ( $n > 0 );
350
	}
351
352
	/**
353
	 * Restore the given (or all) text and file revisions for the page.
354
	 * Once restored, the items will be removed from the archive tables.
355
	 * The deletion log will be updated with an undeletion notice.
356
	 *
357
	 * @param array $timestamps Pass an empty array to restore all revisions,
358
	 *   otherwise list the ones to undelete.
359
	 * @param string $comment
360
	 * @param array $fileVersions
361
	 * @param bool $unsuppress
362
	 * @param User $user User performing the action, or null to use $wgUser
363
	 * @param string|string[] $tags Change tags to add to log entry
364
	 *   ($user should be able to add the specified tags before this is called)
365
	 * @return array(number of file revisions restored, number of image revisions
0 ignored issues
show
Documentation introduced by
The doc-type array(number could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
366
	 *   restored, log message) on success, false on failure.
367
	 */
368
	function undelete( $timestamps, $comment = '', $fileVersions = [],
369
		$unsuppress = false, User $user = null, $tags = null
370
	) {
371
		// If both the set of text revisions and file revisions are empty,
372
		// restore everything. Otherwise, just restore the requested items.
373
		$restoreAll = empty( $timestamps ) && empty( $fileVersions );
374
375
		$restoreText = $restoreAll || !empty( $timestamps );
376
		$restoreFiles = $restoreAll || !empty( $fileVersions );
377
378
		if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
379
			$img = wfLocalFile( $this->title );
380
			$img->load( File::READ_LATEST );
381
			$this->fileStatus = $img->restore( $fileVersions, $unsuppress );
382
			if ( !$this->fileStatus->isOK() ) {
383
				return false;
384
			}
385
			$filesRestored = $this->fileStatus->successCount;
386
		} else {
387
			$filesRestored = 0;
388
		}
389
390
		if ( $restoreText ) {
391
			$this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
392
			if ( !$this->revisionStatus->isOK() ) {
393
				return false;
394
			}
395
396
			$textRestored = $this->revisionStatus->getValue();
397
		} else {
398
			$textRestored = 0;
399
		}
400
401
		// Touch the log!
402
403
		if ( $textRestored && $filesRestored ) {
404
			$reason = wfMessage( 'undeletedrevisions-files' )
405
				->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
406
		} elseif ( $textRestored ) {
407
			$reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
408
				->inContentLanguage()->text();
409
		} elseif ( $filesRestored ) {
410
			$reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
411
				->inContentLanguage()->text();
412
		} else {
413
			wfDebug( "Undelete: nothing undeleted...\n" );
414
415
			return false;
416
		}
417
418
		if ( trim( $comment ) != '' ) {
419
			$reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
420
		}
421
422
		if ( $user === null ) {
423
			global $wgUser;
424
			$user = $wgUser;
425
		}
426
427
		$logEntry = new ManualLogEntry( 'delete', 'restore' );
428
		$logEntry->setPerformer( $user );
429
		$logEntry->setTarget( $this->title );
430
		$logEntry->setComment( $reason );
431
		$logEntry->setTags( $tags );
0 ignored issues
show
Bug introduced by
It seems like $tags defined by parameter $tags on line 369 can also be of type null; however, ManualLogEntry::setTags() does only seem to accept string|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
432
433
		Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
434
435
		$logid = $logEntry->insert();
436
		$logEntry->publish( $logid );
437
438
		return [ $textRestored, $filesRestored, $reason ];
439
	}
440
441
	/**
442
	 * This is the meaty bit -- restores archived revisions of the given page
443
	 * to the cur/old tables. If the page currently exists, all revisions will
444
	 * be stuffed into old, otherwise the most recent will go into cur.
445
	 *
446
	 * @param array $timestamps Pass an empty array to restore all revisions,
447
	 *   otherwise list the ones to undelete.
448
	 * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
449
	 * @param string $comment
450
	 * @throws ReadOnlyError
451
	 * @return Status Status object containing the number of revisions restored on success
452
	 */
453
	private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
454
		if ( wfReadOnly() ) {
455
			throw new ReadOnlyError();
456
		}
457
458
		$restoreAll = empty( $timestamps );
459
		$dbw = wfGetDB( DB_MASTER );
460
461
		# Does this page already exist? We'll have to update it...
462
		$article = WikiPage::factory( $this->title );
463
		# Load latest data for the current page (bug 31179)
464
		$article->loadPageData( 'fromdbmaster' );
465
		$oldcountable = $article->isCountable();
466
467
		$page = $dbw->selectRow( 'page',
468
			[ 'page_id', 'page_latest' ],
469
			[ 'page_namespace' => $this->title->getNamespace(),
470
				'page_title' => $this->title->getDBkey() ],
471
			__METHOD__,
472
			[ 'FOR UPDATE' ] // lock page
473
		);
474
475
		if ( $page ) {
476
			$makepage = false;
477
			# Page already exists. Import the history, and if necessary
478
			# we'll update the latest revision field in the record.
479
480
			$previousRevId = $page->page_latest;
481
482
			# Get the time span of this page
483
			$previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
484
				[ 'rev_id' => $previousRevId ],
485
				__METHOD__ );
486
487 View Code Duplication
			if ( $previousTimestamp === false ) {
488
				wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
489
490
				$status = Status::newGood( 0 );
491
				$status->warning( 'undeleterevision-missing' );
492
493
				return $status;
494
			}
495
		} else {
496
			# Have to create a new article...
497
			$makepage = true;
498
			$previousRevId = 0;
499
			$previousTimestamp = 0;
500
		}
501
502
		$oldWhere = [
503
			'ar_namespace' => $this->title->getNamespace(),
504
			'ar_title' => $this->title->getDBkey(),
505
		];
506
		if ( !$restoreAll ) {
507
			$oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
508
		}
509
510
		$fields = [
511
			'ar_rev_id',
512
			'ar_text',
513
			'ar_comment',
514
			'ar_user',
515
			'ar_user_text',
516
			'ar_timestamp',
517
			'ar_minor_edit',
518
			'ar_flags',
519
			'ar_text_id',
520
			'ar_deleted',
521
			'ar_page_id',
522
			'ar_len',
523
			'ar_sha1'
524
		];
525
526
		if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
527
			$fields[] = 'ar_content_format';
528
			$fields[] = 'ar_content_model';
529
		}
530
531
		/**
532
		 * Select each archived revision...
533
		 */
534
		$result = $dbw->select( 'archive',
535
			$fields,
536
			$oldWhere,
537
			__METHOD__,
538
			/* options */ [ 'ORDER BY' => 'ar_timestamp' ]
539
		);
540
541
		$rev_count = $result->numRows();
542 View Code Duplication
		if ( !$rev_count ) {
543
			wfDebug( __METHOD__ . ": no revisions to restore\n" );
544
545
			$status = Status::newGood( 0 );
546
			$status->warning( "undelete-no-results" );
547
548
			return $status;
549
		}
550
551
		$result->seek( $rev_count - 1 ); // move to last
552
		$row = $result->fetchObject(); // get newest archived rev
553
		$oldPageId = (int)$row->ar_page_id; // pass this to ArticleUndelete hook
554
		$result->seek( 0 ); // move back
555
556
		// grab the content to check consistency with global state before restoring the page.
557
		$revision = Revision::newFromArchiveRow( $row,
558
			[
559
				'title' => $article->getTitle(), // used to derive default content model
560
			]
561
		);
562
		$user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
563
		$content = $revision->getContent( Revision::RAW );
564
565
		// NOTE: article ID may not be known yet. prepareSave() should not modify the database.
566
		$status = $content->prepareSave( $article, 0, -1, $user );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($revi...\Revision::RAW), false) on line 562 can also be of type false; however, Content::prepareSave() does only seem to accept object<User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
567
568
		if ( !$status->isOK() ) {
569
			return $status;
570
		}
571
572
		if ( $makepage ) {
573
			// Check the state of the newest to-be version...
574
			if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
575
				return Status::newFatal( "undeleterevdel" );
576
			}
577
			// Safe to insert now...
578
			$newid = $article->insertOn( $dbw, $row->ar_page_id );
579
			if ( $newid === false ) {
580
				// The old ID is reserved; let's pick another
581
				$newid = $article->insertOn( $dbw );
582
			}
583
			$pageId = $newid;
584
		} else {
585
			// Check if a deleted revision will become the current revision...
586
			if ( $row->ar_timestamp > $previousTimestamp ) {
587
				// Check the state of the newest to-be version...
588
				if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
589
					return Status::newFatal( "undeleterevdel" );
590
				}
591
			}
592
593
			$newid = false;
594
			$pageId = $article->getId();
595
		}
596
597
		$revision = null;
598
		$restored = 0;
599
600
		foreach ( $result as $row ) {
0 ignored issues
show
Bug introduced by
The expression $result 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...
601
			// Check for key dupes due to needed archive integrity.
602
			if ( $row->ar_rev_id ) {
603
				$exists = $dbw->selectField( 'revision', '1',
604
					[ 'rev_id' => $row->ar_rev_id ], __METHOD__ );
605
				if ( $exists ) {
606
					continue; // don't throw DB errors
607
				}
608
			}
609
			// Insert one revision at a time...maintaining deletion status
610
			// unless we are specifically removing all restrictions...
611
			$revision = Revision::newFromArchiveRow( $row,
612
				[
613
					'page' => $pageId,
614
					'title' => $this->title,
615
					'deleted' => $unsuppress ? 0 : $row->ar_deleted
616
				] );
617
618
			$revision->insertOn( $dbw );
619
			$restored++;
620
621
			Hooks::run( 'ArticleRevisionUndeleted', [ &$this->title, $revision, $row->ar_page_id ] );
622
		}
623
		# Now that it's safely stored, take it out of the archive
624
		$dbw->delete( 'archive',
625
			$oldWhere,
626
			__METHOD__ );
627
628
		// Was anything restored at all?
629
		if ( $restored == 0 ) {
630
			return Status::newGood( 0 );
631
		}
632
633
		$created = (bool)$newid;
634
635
		// Attach the latest revision to the page...
636
		$wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
0 ignored issues
show
Bug introduced by
It seems like $revision defined by null on line 597 can be null; however, WikiPage::updateIfNewerOn() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method WikiPage::updateIfNewerOn() has been deprecated with message: since 1.24, use updateRevisionOn instead

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

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

Loading history...
637
		if ( $created || $wasnew ) {
638
			// Update site stats, link tables, etc
639
			$article->doEditUpdates(
640
				$revision,
0 ignored issues
show
Bug introduced by
It seems like $revision defined by null on line 597 can be null; however, WikiPage::doEditUpdates() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
641
				User::newFromName( $revision->getUserText( Revision::RAW ), false ),
0 ignored issues
show
Security Bug introduced by
It seems like \User::newFromName($revi...\Revision::RAW), false) targeting User::newFromName() can also be of type false; however, WikiPage::doEditUpdates() does only seem to accept object<User>, did you maybe forget to handle an error condition?
Loading history...
642
				[
643
					'created' => $created,
644
					'oldcountable' => $oldcountable,
645
					'restored' => true
646
				]
647
			);
648
		}
649
650
		Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
651
652
		if ( $this->title->getNamespace() == NS_FILE ) {
653
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
654
		}
655
656
		return Status::newGood( $restored );
657
	}
658
659
	/**
660
	 * @return Status
661
	 */
662
	function getFileStatus() {
663
		return $this->fileStatus;
664
	}
665
666
	/**
667
	 * @return Status
668
	 */
669
	function getRevisionStatus() {
670
		return $this->revisionStatus;
671
	}
672
}
673
674
/**
675
 * Special page allowing users with the appropriate permissions to view
676
 * and restore deleted content.
677
 *
678
 * @ingroup SpecialPage
679
 */
680
class SpecialUndelete extends SpecialPage {
681
	private	$mAction;
682
	private	$mTarget;
683
	private	$mTimestamp;
684
	private	$mRestore;
685
	private	$mRevdel;
686
	private	$mInvert;
687
	private	$mFilename;
688
	private	$mTargetTimestamp;
689
	private	$mAllowed;
690
	private	$mCanView;
691
	private	$mComment;
692
	private	$mToken;
693
694
	/** @var Title */
695
	private $mTargetObj;
696
697
	function __construct() {
698
		parent::__construct( 'Undelete', 'deletedhistory' );
699
	}
700
701
	public function doesWrites() {
702
		return true;
703
	}
704
705
	function loadRequest( $par ) {
706
		$request = $this->getRequest();
707
		$user = $this->getUser();
708
709
		$this->mAction = $request->getVal( 'action' );
710 View Code Duplication
		if ( $par !== null && $par !== '' ) {
711
			$this->mTarget = $par;
712
		} else {
713
			$this->mTarget = $request->getVal( 'target' );
714
		}
715
716
		$this->mTargetObj = null;
717
718
		if ( $this->mTarget !== null && $this->mTarget !== '' ) {
719
			$this->mTargetObj = Title::newFromText( $this->mTarget );
720
		}
721
722
		$this->mSearchPrefix = $request->getText( 'prefix' );
0 ignored issues
show
Bug introduced by
The property mSearchPrefix 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...
723
		$time = $request->getVal( 'timestamp' );
724
		$this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
725
		$this->mFilename = $request->getVal( 'file' );
726
727
		$posted = $request->wasPosted() &&
728
			$user->matchEditToken( $request->getVal( 'wpEditToken' ) );
729
		$this->mRestore = $request->getCheck( 'restore' ) && $posted;
730
		$this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
731
		$this->mInvert = $request->getCheck( 'invert' ) && $posted;
732
		$this->mPreview = $request->getCheck( 'preview' ) && $posted;
0 ignored issues
show
Bug introduced by
The property mPreview 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...
733
		$this->mDiff = $request->getCheck( 'diff' );
0 ignored issues
show
Bug introduced by
The property mDiff 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...
734
		$this->mDiffOnly = $request->getBool( 'diffonly', $this->getUser()->getOption( 'diffonly' ) );
0 ignored issues
show
Bug introduced by
The property mDiffOnly 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...
735
		$this->mComment = $request->getText( 'wpComment' );
736
		$this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $user->isAllowed( 'suppressrevision' );
0 ignored issues
show
Bug introduced by
The property mUnsuppress 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...
Bug Best Practice introduced by
The expression $request->getVal('wpUnsuppress') of type null|string is loosely compared to true; 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...
737
		$this->mToken = $request->getVal( 'token' );
738
739
		if ( $this->isAllowed( 'undelete' ) && !$user->isBlocked() ) {
740
			$this->mAllowed = true; // user can restore
741
			$this->mCanView = true; // user can view content
742
		} elseif ( $this->isAllowed( 'deletedtext' ) ) {
743
			$this->mAllowed = false; // user cannot restore
744
			$this->mCanView = true; // user can view content
745
			$this->mRestore = false;
746
		} else { // user can only view the list of revisions
747
			$this->mAllowed = false;
748
			$this->mCanView = false;
749
			$this->mTimestamp = '';
750
			$this->mRestore = false;
751
		}
752
753
		if ( $this->mRestore || $this->mInvert ) {
754
			$timestamps = [];
755
			$this->mFileVersions = [];
0 ignored issues
show
Bug introduced by
The property mFileVersions 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...
756
			foreach ( $request->getValues() as $key => $val ) {
757
				$matches = [];
758
				if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
759
					array_push( $timestamps, $matches[1] );
760
				}
761
762
				if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
763
					$this->mFileVersions[] = intval( $matches[1] );
764
				}
765
			}
766
			rsort( $timestamps );
767
			$this->mTargetTimestamp = $timestamps;
768
		}
769
	}
770
771
	/**
772
	 * Checks whether a user is allowed the permission for the
773
	 * specific title if one is set.
774
	 *
775
	 * @param string $permission
776
	 * @param User $user
777
	 * @return bool
778
	 */
779
	protected function isAllowed( $permission, User $user = null ) {
780
		$user = $user ?: $this->getUser();
781
		if ( $this->mTargetObj !== null ) {
782
			return $this->mTargetObj->userCan( $permission, $user );
783
		} else {
784
			return $user->isAllowed( $permission );
785
		}
786
	}
787
788
	function userCanExecute( User $user ) {
789
		return $this->isAllowed( $this->mRestriction, $user );
790
	}
791
792
	function execute( $par ) {
793
		$this->useTransactionalTimeLimit();
794
795
		$user = $this->getUser();
796
797
		$this->setHeaders();
798
		$this->outputHeader();
799
800
		$this->loadRequest( $par );
801
		$this->checkPermissions(); // Needs to be after mTargetObj is set
802
803
		$out = $this->getOutput();
804
805
		if ( is_null( $this->mTargetObj ) ) {
806
			$out->addWikiMsg( 'undelete-header' );
807
808
			# Not all users can just browse every deleted page from the list
809
			if ( $user->isAllowed( 'browsearchive' ) ) {
810
				$this->showSearchForm();
811
			}
812
813
			return;
814
		}
815
816
		$this->addHelpLink( 'Help:Undelete' );
817
		if ( $this->mAllowed ) {
818
			$out->setPageTitle( $this->msg( 'undeletepage' ) );
819
		} else {
820
			$out->setPageTitle( $this->msg( 'viewdeletedpage' ) );
821
		}
822
823
		$this->getSkin()->setRelevantTitle( $this->mTargetObj );
824
825
		if ( $this->mTimestamp !== '' ) {
826
			$this->showRevision( $this->mTimestamp );
827
		} elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
828
			$file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
829
			// Check if user is allowed to see this file
830
			if ( !$file->exists() ) {
831
				$out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
832 View Code Duplication
			} elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
833
				if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
834
					throw new PermissionsError( 'suppressrevision' );
835
				} else {
836
					throw new PermissionsError( 'deletedtext' );
837
				}
838
			} elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
839
				$this->showFileConfirmationForm( $this->mFilename );
840
			} else {
841
				$this->showFile( $this->mFilename );
842
			}
843
		} elseif ( $this->mAction === "submit" ) {
844
			if ( $this->mRestore ) {
845
				$this->undelete();
846
			} elseif ( $this->mRevdel ) {
847
				$this->redirectToRevDel();
848
			}
849
850
		} else {
851
			$this->showHistory();
852
		}
853
	}
854
855
	/**
856
	 * Convert submitted form data to format expected by RevisionDelete and
857
	 * redirect the request
858
	 */
859
	private function redirectToRevDel() {
860
		$archive = new PageArchive( $this->mTargetObj );
861
862
		$revisions = [];
863
864
		foreach ( $this->getRequest()->getValues() as $key => $val ) {
865
			$matches = [];
866
			if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
867
				$revisions[ $archive->getRevision( $matches[1] )->getId() ] = 1;
868
			}
869
		}
870
		$query = [
871
			"type" => "revision",
872
			"ids" => $revisions,
873
			"target" => $this->mTargetObj->getPrefixedText()
874
		];
875
		$url = SpecialPage::getTitleFor( "RevisionDelete" )->getFullURL( $query );
876
		$this->getOutput()->redirect( $url );
877
	}
878
879
	function showSearchForm() {
880
		$out = $this->getOutput();
881
		$out->setPageTitle( $this->msg( 'undelete-search-title' ) );
882
		$out->addHTML(
883
			Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ) .
884
				Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) .
885
				Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
886
				Html::rawElement(
887
					'label',
888
					[ 'for' => 'prefix' ],
889
					$this->msg( 'undelete-search-prefix' )->parse()
890
				) .
891
				Xml::input(
892
					'prefix',
893
					20,
894
					$this->mSearchPrefix,
895
					[ 'id' => 'prefix', 'autofocus' => '' ]
896
				) . ' ' .
897
				Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) .
898
				Xml::closeElement( 'fieldset' ) .
899
				Xml::closeElement( 'form' )
900
		);
901
902
		# List undeletable articles
903
		if ( $this->mSearchPrefix ) {
904
			$result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
905
			$this->showList( $result );
906
		}
907
	}
908
909
	/**
910
	 * Generic list of deleted pages
911
	 *
912
	 * @param ResultWrapper $result
913
	 * @return bool
914
	 */
915
	private function showList( $result ) {
916
		$out = $this->getOutput();
917
918
		if ( $result->numRows() == 0 ) {
919
			$out->addWikiMsg( 'undelete-no-results' );
920
921
			return false;
922
		}
923
924
		$out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
925
926
		$undelete = $this->getPageTitle();
927
		$out->addHTML( "<ul>\n" );
928
		foreach ( $result as $row ) {
929
			$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
930
			if ( $title !== null ) {
931
				$item = Linker::linkKnown(
932
					$undelete,
933
					htmlspecialchars( $title->getPrefixedText() ),
934
					[],
935
					[ 'target' => $title->getPrefixedText() ]
936
				);
937
			} else {
938
				// The title is no longer valid, show as text
939
				$item = Html::element(
940
					'span',
941
					[ 'class' => 'mw-invalidtitle' ],
942
					Linker::getInvalidTitleDescription(
943
						$this->getContext(),
944
						$row->ar_namespace,
945
						$row->ar_title
946
					)
947
				);
948
			}
949
			$revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
950
			$out->addHTML( "<li>{$item} ({$revs})</li>\n" );
951
		}
952
		$result->free();
953
		$out->addHTML( "</ul>\n" );
954
955
		return true;
956
	}
957
958
	private function showRevision( $timestamp ) {
959
		if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
960
			return;
961
		}
962
963
		$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
964
		if ( !Hooks::run( 'UndeleteForm::showRevision', [ &$archive, $this->mTargetObj ] ) ) {
965
			return;
966
		}
967
		$rev = $archive->getRevision( $timestamp );
968
969
		$out = $this->getOutput();
970
		$user = $this->getUser();
971
972
		if ( !$rev ) {
973
			$out->addWikiMsg( 'undeleterevision-missing' );
974
975
			return;
976
		}
977
978
		if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
979 View Code Duplication
			if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
980
				$out->wrapWikiMsg(
981
					"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
982
				$rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
983
					'rev-suppressed-text-permission' : 'rev-deleted-text-permission'
984
				);
985
986
				return;
987
			}
988
989
			$out->wrapWikiMsg(
990
				"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
991
				$rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
992
					'rev-suppressed-text-view' : 'rev-deleted-text-view'
993
			);
994
			$out->addHTML( '<br />' );
995
			// and we are allowed to see...
996
		}
997
998
		if ( $this->mDiff ) {
999
			$previousRev = $archive->getPreviousRevision( $timestamp );
1000
			if ( $previousRev ) {
1001
				$this->showDiff( $previousRev, $rev );
1002
				if ( $this->mDiffOnly ) {
1003
					return;
1004
				}
1005
1006
				$out->addHTML( '<hr />' );
1007
			} else {
1008
				$out->addWikiMsg( 'undelete-nodiff' );
1009
			}
1010
		}
1011
1012
		$link = Linker::linkKnown(
1013
			$this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
1014
			htmlspecialchars( $this->mTargetObj->getPrefixedText() )
1015
		);
1016
1017
		$lang = $this->getLanguage();
1018
1019
		// date and time are separate parameters to facilitate localisation.
1020
		// $time is kept for backward compat reasons.
1021
		$time = $lang->userTimeAndDate( $timestamp, $user );
1022
		$d = $lang->userDate( $timestamp, $user );
1023
		$t = $lang->userTime( $timestamp, $user );
1024
		$userLink = Linker::revUserTools( $rev );
1025
1026
		$content = $rev->getContent( Revision::FOR_THIS_USER, $user );
1027
1028
		$isText = ( $content instanceof TextContent );
1029
1030
		if ( $this->mPreview || $isText ) {
1031
			$openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
1032
		} else {
1033
			$openDiv = '<div id="mw-undelete-revision">';
1034
		}
1035
		$out->addHTML( $openDiv );
1036
1037
		// Revision delete links
1038
		if ( !$this->mDiff ) {
1039
			$revdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
1040
			if ( $revdel ) {
1041
				$out->addHTML( "$revdel " );
1042
			}
1043
		}
1044
1045
		$out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
1046
			$time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
1047
1048
		if ( !Hooks::run( 'UndeleteShowRevision', [ $this->mTargetObj, $rev ] ) ) {
1049
			return;
1050
		}
1051
1052
		if ( ( $this->mPreview || !$isText ) && $content ) {
1053
			// NOTE: non-text content has no source view, so always use rendered preview
1054
1055
			// Hide [edit]s
1056
			$popts = $out->parserOptions();
1057
			$popts->setEditSection( false );
1058
1059
			$pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
1060
			$out->addParserOutput( $pout );
1061
		}
1062
1063
		if ( $isText ) {
1064
			// source view for textual content
1065
			$sourceView = Xml::element(
1066
				'textarea',
1067
				[
1068
					'readonly' => 'readonly',
1069
					'cols' => $user->getIntOption( 'cols' ),
1070
					'rows' => $user->getIntOption( 'rows' )
1071
				],
1072
				$content->getNativeData() . "\n"
1073
			);
1074
1075
			$previewButton = Xml::element( 'input', [
1076
				'type' => 'submit',
1077
				'name' => 'preview',
1078
				'value' => $this->msg( 'showpreview' )->text()
1079
			] );
1080
		} else {
1081
			$sourceView = '';
1082
			$previewButton = '';
1083
		}
1084
1085
		$diffButton = Xml::element( 'input', [
1086
			'name' => 'diff',
1087
			'type' => 'submit',
1088
			'value' => $this->msg( 'showdiff' )->text() ] );
1089
1090
		$out->addHTML(
1091
			$sourceView .
1092
				Xml::openElement( 'div', [
1093
					'style' => 'clear: both' ] ) .
1094
				Xml::openElement( 'form', [
1095
					'method' => 'post',
1096
					'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
1097
				Xml::element( 'input', [
1098
					'type' => 'hidden',
1099
					'name' => 'target',
1100
					'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
1101
				Xml::element( 'input', [
1102
					'type' => 'hidden',
1103
					'name' => 'timestamp',
1104
					'value' => $timestamp ] ) .
1105
				Xml::element( 'input', [
1106
					'type' => 'hidden',
1107
					'name' => 'wpEditToken',
1108
					'value' => $user->getEditToken() ] ) .
1109
				$previewButton .
1110
				$diffButton .
1111
				Xml::closeElement( 'form' ) .
1112
				Xml::closeElement( 'div' )
1113
		);
1114
	}
1115
1116
	/**
1117
	 * Build a diff display between this and the previous either deleted
1118
	 * or non-deleted edit.
1119
	 *
1120
	 * @param Revision $previousRev
1121
	 * @param Revision $currentRev
1122
	 * @return string HTML
1123
	 */
1124
	function showDiff( $previousRev, $currentRev ) {
1125
		$diffContext = clone $this->getContext();
1126
		$diffContext->setTitle( $currentRev->getTitle() );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1127
		$diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
0 ignored issues
show
Bug introduced by
It seems like $currentRev->getTitle() can be null; however, factory() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setWikiPage() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1128
1129
		$diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
1130
		$diffEngine->showDiffStyle();
1131
1132
		$formattedDiff = $diffEngine->generateContentDiffBody(
1133
			$previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
0 ignored issues
show
Bug introduced by
It seems like $previousRev->getContent...USER, $this->getUser()) can be null; however, generateContentDiffBody() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1134
			$currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
0 ignored issues
show
Bug introduced by
It seems like $currentRev->getContent(...USER, $this->getUser()) can be null; however, generateContentDiffBody() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1135
		);
1136
1137
		$formattedDiff = $diffEngine->addHeader(
1138
			$formattedDiff,
1139
			$this->diffHeader( $previousRev, 'o' ),
1140
			$this->diffHeader( $currentRev, 'n' )
1141
		);
1142
1143
		$this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
1144
	}
1145
1146
	/**
1147
	 * @param Revision $rev
1148
	 * @param string $prefix
1149
	 * @return string
1150
	 */
1151
	private function diffHeader( $rev, $prefix ) {
1152
		$isDeleted = !( $rev->getId() && $rev->getTitle() );
0 ignored issues
show
Bug Best Practice introduced by
The expression $rev->getId() of type integer|null 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...
1153
		if ( $isDeleted ) {
1154
			/// @todo FIXME: $rev->getTitle() is null for deleted revs...?
1155
			$targetPage = $this->getPageTitle();
1156
			$targetQuery = [
1157
				'target' => $this->mTargetObj->getPrefixedText(),
1158
				'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() )
1159
			];
1160
		} else {
1161
			/// @todo FIXME: getId() may return non-zero for deleted revs...
1162
			$targetPage = $rev->getTitle();
1163
			$targetQuery = [ 'oldid' => $rev->getId() ];
1164
		}
1165
1166
		// Add show/hide deletion links if available
1167
		$user = $this->getUser();
1168
		$lang = $this->getLanguage();
1169
		$rdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
1170
1171
		if ( $rdel ) {
1172
			$rdel = " $rdel";
1173
		}
1174
1175
		$minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
1176
1177
		$tags = wfGetDB( DB_SLAVE )->selectField(
1178
			'tag_summary',
1179
			'ts_tags',
1180
			[ 'ts_rev_id' => $rev->getId() ],
1181
			__METHOD__
1182
		);
1183
		$tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
1184
1185
		// FIXME This is reimplementing DifferenceEngine#getRevisionHeader
1186
		// and partially #showDiffPage, but worse
1187
		return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
1188
			Linker::link(
1189
				$targetPage,
1190
				$this->msg(
1191
					'revisionasof',
1192
					$lang->userTimeAndDate( $rev->getTimestamp(), $user ),
1193
					$lang->userDate( $rev->getTimestamp(), $user ),
1194
					$lang->userTime( $rev->getTimestamp(), $user )
1195
				)->escaped(),
1196
				[],
1197
				$targetQuery
1198
			) .
1199
			'</strong></div>' .
1200
			'<div id="mw-diff-' . $prefix . 'title2">' .
1201
			Linker::revUserTools( $rev ) . '<br />' .
1202
			'</div>' .
1203
			'<div id="mw-diff-' . $prefix . 'title3">' .
1204
			$minor . Linker::revComment( $rev ) . $rdel . '<br />' .
1205
			'</div>' .
1206
			'<div id="mw-diff-' . $prefix . 'title5">' .
1207
			$tagSummary[0] . '<br />' .
1208
			'</div>';
1209
	}
1210
1211
	/**
1212
	 * Show a form confirming whether a tokenless user really wants to see a file
1213
	 * @param string $key
1214
	 */
1215
	private function showFileConfirmationForm( $key ) {
1216
		$out = $this->getOutput();
1217
		$lang = $this->getLanguage();
1218
		$user = $this->getUser();
1219
		$file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
1220
		$out->addWikiMsg( 'undelete-show-file-confirm',
1221
			$this->mTargetObj->getText(),
1222
			$lang->userDate( $file->getTimestamp(), $user ),
1223
			$lang->userTime( $file->getTimestamp(), $user ) );
1224
		$out->addHTML(
1225
			Xml::openElement( 'form', [
1226
					'method' => 'POST',
1227
					'action' => $this->getPageTitle()->getLocalURL( [
1228
						'target' => $this->mTarget,
1229
						'file' => $key,
1230
						'token' => $user->getEditToken( $key ),
1231
					] ),
1232
				]
1233
			) .
1234
				Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
1235
				'</form>'
1236
		);
1237
	}
1238
1239
	/**
1240
	 * Show a deleted file version requested by the visitor.
1241
	 * @param string $key
1242
	 */
1243
	private function showFile( $key ) {
1244
		$this->getOutput()->disable();
1245
1246
		# We mustn't allow the output to be CDN cached, otherwise
1247
		# if an admin previews a deleted image, and it's cached, then
1248
		# a user without appropriate permissions can toddle off and
1249
		# nab the image, and CDN will serve it
1250
		$response = $this->getRequest()->response();
1251
		$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
1252
		$response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
1253
		$response->header( 'Pragma: no-cache' );
1254
1255
		$repo = RepoGroup::singleton()->getLocalRepo();
1256
		$path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
1257
		$repo->streamFile( $path );
0 ignored issues
show
Deprecated Code introduced by
The method FileRepo::streamFile() has been deprecated with message: since 1.26, use streamFileWithStatus

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

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

Loading history...
1258
	}
1259
1260
	protected function showHistory() {
1261
		$this->checkReadOnly();
1262
1263
		$out = $this->getOutput();
1264
		if ( $this->mAllowed ) {
1265
			$out->addModules( 'mediawiki.special.undelete' );
1266
		}
1267
		$out->wrapWikiMsg(
1268
			"<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1269
			[ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1270
		);
1271
1272
		$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1273
		Hooks::run( 'UndeleteForm::showHistory', [ &$archive, $this->mTargetObj ] );
1274
		/*
1275
		$text = $archive->getLastRevisionText();
1276
		if( is_null( $text ) ) {
1277
			$out->addWikiMsg( 'nohistory' );
1278
			return;
1279
		}
1280
		*/
1281
		$out->addHTML( '<div class="mw-undelete-history">' );
1282
		if ( $this->mAllowed ) {
1283
			$out->addWikiMsg( 'undeletehistory' );
1284
			$out->addWikiMsg( 'undeleterevdel' );
1285
		} else {
1286
			$out->addWikiMsg( 'undeletehistorynoadmin' );
1287
		}
1288
		$out->addHTML( '</div>' );
1289
1290
		# List all stored revisions
1291
		$revisions = $archive->listRevisions();
1292
		$files = $archive->listFiles();
1293
1294
		$haveRevisions = $revisions && $revisions->numRows() > 0;
1295
		$haveFiles = $files && $files->numRows() > 0;
1296
1297
		# Batch existence check on user and talk pages
1298 View Code Duplication
		if ( $haveRevisions ) {
1299
			$batch = new LinkBatch();
1300
			foreach ( $revisions as $row ) {
1301
				$batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::makeTitleSafe(NS...ER, $row->ar_user_text) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1302
				$batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::makeTitleSafe(NS...LK, $row->ar_user_text) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1303
			}
1304
			$batch->execute();
1305
			$revisions->seek( 0 );
1306
		}
1307 View Code Duplication
		if ( $haveFiles ) {
1308
			$batch = new LinkBatch();
1309
			foreach ( $files as $row ) {
1310
				$batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::makeTitleSafe(NS...ER, $row->fa_user_text) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1311
				$batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
0 ignored issues
show
Bug introduced by
It seems like \Title::makeTitleSafe(NS...LK, $row->fa_user_text) can be null; however, addObj() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1312
			}
1313
			$batch->execute();
1314
			$files->seek( 0 );
1315
		}
1316
1317
		if ( $this->mAllowed ) {
1318
			$action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
1319
			# Start the form here
1320
			$top = Xml::openElement(
1321
				'form',
1322
				[ 'method' => 'post', 'action' => $action, 'id' => 'undelete' ]
1323
			);
1324
			$out->addHTML( $top );
1325
		}
1326
1327
		# Show relevant lines from the deletion log:
1328
		$deleteLogPage = new LogPage( 'delete' );
1329
		$out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
1330
		LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
1331
		# Show relevant lines from the suppression log:
1332
		$suppressLogPage = new LogPage( 'suppress' );
1333
		if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
1334
			$out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
1335
			LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
1336
		}
1337
1338
		if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1339
			# Format the user-visible controls (comment field, submission button)
1340
			# in a nice little table
1341
			if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
1342
				$unsuppressBox =
1343
					"<tr>
1344
						<td>&#160;</td>
1345
						<td class='mw-input'>" .
1346
						Xml::checkLabel( $this->msg( 'revdelete-unsuppress' )->text(),
1347
							'wpUnsuppress', 'mw-undelete-unsuppress', $this->mUnsuppress ) .
1348
						"</td>
1349
					</tr>";
1350
			} else {
1351
				$unsuppressBox = '';
1352
			}
1353
1354
			$table = Xml::fieldset( $this->msg( 'undelete-fieldset-title' )->text() ) .
1355
				Xml::openElement( 'table', [ 'id' => 'mw-undelete-table' ] ) .
1356
				"<tr>
1357
					<td colspan='2' class='mw-undelete-extrahelp'>" .
1358
				$this->msg( 'undeleteextrahelp' )->parseAsBlock() .
1359
				"</td>
1360
			</tr>
1361
			<tr>
1362
				<td class='mw-label'>" .
1363
				Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) .
1364
				"</td>
1365
				<td class='mw-input'>" .
1366
				Xml::input(
1367
					'wpComment',
1368
					50,
1369
					$this->mComment,
1370
					[ 'id' => 'wpComment', 'autofocus' => '' ]
1371
				) .
1372
				"</td>
1373
			</tr>
1374
			<tr>
1375
				<td>&#160;</td>
1376
				<td class='mw-submit'>" .
1377
				Xml::submitButton(
1378
					$this->msg( 'undeletebtn' )->text(),
1379
					[ 'name' => 'restore', 'id' => 'mw-undelete-submit' ]
1380
				) . ' ' .
1381
				Xml::submitButton(
1382
					$this->msg( 'undeleteinvert' )->text(),
1383
					[ 'name' => 'invert', 'id' => 'mw-undelete-invert' ]
1384
				) .
1385
				"</td>
1386
			</tr>" .
1387
				$unsuppressBox .
1388
				Xml::closeElement( 'table' ) .
1389
				Xml::closeElement( 'fieldset' );
1390
1391
			$out->addHTML( $table );
1392
		}
1393
1394
		$out->addHTML( Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n" );
1395
1396
		if ( $haveRevisions ) {
1397
			# Show the page's stored (deleted) history
1398
1399
			if ( $this->getUser()->isAllowed( 'deleterevision' ) ) {
1400
				$out->addHTML( Html::element(
1401
					'button',
1402
					[
1403
						'name' => 'revdel',
1404
						'type' => 'submit',
1405
						'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
1406
					],
1407
					$this->msg( 'showhideselectedversions' )->text()
1408
				) . "\n" );
1409
			}
1410
1411
			$out->addHTML( '<ul>' );
1412
			$remaining = $revisions->numRows();
1413
			$earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
1414
1415
			foreach ( $revisions as $row ) {
1416
				$remaining--;
1417
				$out->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining ) );
1418
			}
1419
			$revisions->free();
1420
			$out->addHTML( '</ul>' );
1421
		} else {
1422
			$out->addWikiMsg( 'nohistory' );
1423
		}
1424
1425
		if ( $haveFiles ) {
1426
			$out->addHTML( Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n" );
1427
			$out->addHTML( '<ul>' );
1428
			foreach ( $files as $row ) {
1429
				$out->addHTML( $this->formatFileRow( $row ) );
1430
			}
1431
			$files->free();
1432
			$out->addHTML( '</ul>' );
1433
		}
1434
1435
		if ( $this->mAllowed ) {
1436
			# Slip in the hidden controls here
1437
			$misc = Html::hidden( 'target', $this->mTarget );
1438
			$misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
1439
			$misc .= Xml::closeElement( 'form' );
1440
			$out->addHTML( $misc );
1441
		}
1442
1443
		return true;
1444
	}
1445
1446
	protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
1447
		$rev = Revision::newFromArchiveRow( $row,
1448
			[
1449
				'title' => $this->mTargetObj
1450
			] );
1451
1452
		$revTextSize = '';
1453
		$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
1454
		// Build checkboxen...
1455
		if ( $this->mAllowed ) {
1456
			if ( $this->mInvert ) {
1457
				if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1458
					$checkBox = Xml::check( "ts$ts" );
1459
				} else {
1460
					$checkBox = Xml::check( "ts$ts", true );
1461
				}
1462
			} else {
1463
				$checkBox = Xml::check( "ts$ts" );
1464
			}
1465
		} else {
1466
			$checkBox = '';
1467
		}
1468
1469
		// Build page & diff links...
1470
		$user = $this->getUser();
1471
		if ( $this->mCanView ) {
1472
			$titleObj = $this->getPageTitle();
1473
			# Last link
1474
			if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
1475
				$pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1476
				$last = $this->msg( 'diff' )->escaped();
1477
			} elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1478
				$pageLink = $this->getPageLink( $rev, $titleObj, $ts );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by wfTimestamp(TS_MW, $row->ar_timestamp) on line 1453 can also be of type false; however, SpecialUndelete::getPageLink() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1479
				$last = Linker::linkKnown(
1480
					$titleObj,
1481
					$this->msg( 'diff' )->escaped(),
1482
					[],
1483
					[
1484
						'target' => $this->mTargetObj->getPrefixedText(),
1485
						'timestamp' => $ts,
1486
						'diff' => 'prev'
1487
					]
1488
				);
1489
			} else {
1490
				$pageLink = $this->getPageLink( $rev, $titleObj, $ts );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by wfTimestamp(TS_MW, $row->ar_timestamp) on line 1453 can also be of type false; however, SpecialUndelete::getPageLink() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1491
				$last = $this->msg( 'diff' )->escaped();
1492
			}
1493
		} else {
1494
			$pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1495
			$last = $this->msg( 'diff' )->escaped();
1496
		}
1497
1498
		// User links
1499
		$userLink = Linker::revUserTools( $rev );
1500
1501
		// Minor edit
1502
		$minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
1503
1504
		// Revision text size
1505
		$size = $row->ar_len;
1506
		if ( !is_null( $size ) ) {
1507
			$revTextSize = Linker::formatRevisionSize( $size );
1508
		}
1509
1510
		// Edit summary
1511
		$comment = Linker::revComment( $rev );
1512
1513
		// Tags
1514
		$attribs = [];
1515
		list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow(
1516
			$row->ts_tags,
1517
			'deletedhistory',
1518
			$this->getContext()
1519
		);
1520
		if ( $classes ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $classes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1521
			$attribs['class'] = implode( ' ', $classes );
1522
		}
1523
1524
		$revisionRow = $this->msg( 'undelete-revision-row2' )
1525
			->rawParams(
1526
				$checkBox,
1527
				$last,
1528
				$pageLink,
1529
				$userLink,
1530
				$minor,
1531
				$revTextSize,
1532
				$comment,
1533
				$tagSummary
1534
			)
1535
			->escaped();
1536
1537
		return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
1538
	}
1539
1540
	private function formatFileRow( $row ) {
1541
		$file = ArchivedFile::newFromRow( $row );
1542
		$ts = wfTimestamp( TS_MW, $row->fa_timestamp );
1543
		$user = $this->getUser();
1544
1545
		$checkBox = '';
1546
		if ( $this->mCanView && $row->fa_storage_key ) {
1547
			if ( $this->mAllowed ) {
1548
				$checkBox = Xml::check( 'fileid' . $row->fa_id );
1549
			}
1550
			$key = urlencode( $row->fa_storage_key );
1551
			$pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by wfTimestamp(TS_MW, $row->fa_timestamp) on line 1542 can also be of type false; however, SpecialUndelete::getFileLink() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1552
		} else {
1553
			$pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user );
1554
		}
1555
		$userLink = $this->getFileUser( $file );
1556
		$data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1557
		$bytes = $this->msg( 'parentheses' )
1558
			->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
1559
			->plain();
1560
		$data = htmlspecialchars( $data . ' ' . $bytes );
1561
		$comment = $this->getFileComment( $file );
1562
1563
		// Add show/hide deletion links if available
1564
		$canHide = $this->isAllowed( 'deleterevision' );
1565
		if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1566
			if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1567
				// Revision was hidden from sysops
1568
				$revdlink = Linker::revDeleteLinkDisabled( $canHide );
1569
			} else {
1570
				$query = [
1571
					'type' => 'filearchive',
1572
					'target' => $this->mTargetObj->getPrefixedDBkey(),
1573
					'ids' => $row->fa_id
1574
				];
1575
				$revdlink = Linker::revDeleteLink( $query,
1576
					$file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1577
			}
1578
		} else {
1579
			$revdlink = '';
1580
		}
1581
1582
		return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1583
	}
1584
1585
	/**
1586
	 * Fetch revision text link if it's available to all users
1587
	 *
1588
	 * @param Revision $rev
1589
	 * @param Title $titleObj
1590
	 * @param string $ts Timestamp
1591
	 * @return string
1592
	 */
1593 View Code Duplication
	function getPageLink( $rev, $titleObj, $ts ) {
1594
		$user = $this->getUser();
1595
		$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1596
1597
		if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1598
			return '<span class="history-deleted">' . $time . '</span>';
1599
		}
1600
1601
		$link = Linker::linkKnown(
1602
			$titleObj,
1603
			htmlspecialchars( $time ),
1604
			[],
1605
			[
1606
				'target' => $this->mTargetObj->getPrefixedText(),
1607
				'timestamp' => $ts
1608
			]
1609
		);
1610
1611
		if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1612
			$link = '<span class="history-deleted">' . $link . '</span>';
1613
		}
1614
1615
		return $link;
1616
	}
1617
1618
	/**
1619
	 * Fetch image view link if it's available to all users
1620
	 *
1621
	 * @param File|ArchivedFile $file
1622
	 * @param Title $titleObj
1623
	 * @param string $ts A timestamp
1624
	 * @param string $key A storage key
1625
	 *
1626
	 * @return string HTML fragment
1627
	 */
1628 View Code Duplication
	function getFileLink( $file, $titleObj, $ts, $key ) {
1629
		$user = $this->getUser();
1630
		$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1631
1632
		if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1633
			return '<span class="history-deleted">' . $time . '</span>';
1634
		}
1635
1636
		$link = Linker::linkKnown(
1637
			$titleObj,
1638
			htmlspecialchars( $time ),
1639
			[],
1640
			[
1641
				'target' => $this->mTargetObj->getPrefixedText(),
1642
				'file' => $key,
1643
				'token' => $user->getEditToken( $key )
1644
			]
1645
		);
1646
1647
		if ( $file->isDeleted( File::DELETED_FILE ) ) {
1648
			$link = '<span class="history-deleted">' . $link . '</span>';
1649
		}
1650
1651
		return $link;
1652
	}
1653
1654
	/**
1655
	 * Fetch file's user id if it's available to this user
1656
	 *
1657
	 * @param File|ArchivedFile $file
1658
	 * @return string HTML fragment
1659
	 */
1660
	function getFileUser( $file ) {
1661
		if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
1662
			return '<span class="history-deleted">' .
1663
				$this->msg( 'rev-deleted-user' )->escaped() .
1664
				'</span>';
1665
		}
1666
1667
		$link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
0 ignored issues
show
Bug introduced by
The method getRawUser does only exist in ArchivedFile, but not in File.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Bug introduced by
The method getRawUserText does only exist in ArchivedFile, but not in File.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1668
			Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
1669
1670
		if ( $file->isDeleted( File::DELETED_USER ) ) {
1671
			$link = '<span class="history-deleted">' . $link . '</span>';
1672
		}
1673
1674
		return $link;
1675
	}
1676
1677
	/**
1678
	 * Fetch file upload comment if it's available to this user
1679
	 *
1680
	 * @param File|ArchivedFile $file
1681
	 * @return string HTML fragment
1682
	 */
1683
	function getFileComment( $file ) {
1684
		if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
1685
			return '<span class="history-deleted"><span class="comment">' .
1686
				$this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>';
1687
		}
1688
1689
		$link = Linker::commentBlock( $file->getRawDescription() );
0 ignored issues
show
Bug introduced by
The method getRawDescription does only exist in ArchivedFile, but not in File.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1690
1691
		if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1692
			$link = '<span class="history-deleted">' . $link . '</span>';
1693
		}
1694
1695
		return $link;
1696
	}
1697
1698
	function undelete() {
1699
		if ( $this->getConfig()->get( 'UploadMaintenance' )
1700
			&& $this->mTargetObj->getNamespace() == NS_FILE
1701
		) {
1702
			throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1703
		}
1704
1705
		$this->checkReadOnly();
1706
1707
		$out = $this->getOutput();
1708
		$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1709
		Hooks::run( 'UndeleteForm::undelete', [ &$archive, $this->mTargetObj ] );
1710
		$ok = $archive->undelete(
1711
			$this->mTargetTimestamp,
1712
			$this->mComment,
1713
			$this->mFileVersions,
1714
			$this->mUnsuppress,
1715
			$this->getUser()
1716
		);
1717
1718
		if ( is_array( $ok ) ) {
1719
			if ( $ok[1] ) { // Undeleted file count
1720
				Hooks::run( 'FileUndeleteComplete', [
1721
					$this->mTargetObj, $this->mFileVersions,
1722
					$this->getUser(), $this->mComment ] );
1723
			}
1724
1725
			$link = Linker::linkKnown( $this->mTargetObj );
1726
			$out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
1727
		} else {
1728
			$out->setPageTitle( $this->msg( 'undelete-error' ) );
1729
		}
1730
1731
		// Show revision undeletion warnings and errors
1732
		$status = $archive->getRevisionStatus();
1733
		if ( $status && !$status->isGood() ) {
1734
			$out->addWikiText( '<div class="error">' .
1735
				$status->getWikiText(
1736
					'cannotundelete',
1737
					'cannotundelete'
1738
				) . '</div>'
1739
			);
1740
		}
1741
1742
		// Show file undeletion warnings and errors
1743
		$status = $archive->getFileStatus();
1744
		if ( $status && !$status->isGood() ) {
1745
			$out->addWikiText( '<div class="error">' .
1746
				$status->getWikiText(
1747
					'undelete-error-short',
1748
					'undelete-error-long'
1749
				) . '</div>'
1750
			);
1751
		}
1752
	}
1753
1754
	/**
1755
	 * Return an array of subpages beginning with $search that this special page will accept.
1756
	 *
1757
	 * @param string $search Prefix to search for
1758
	 * @param int $limit Maximum number of results to return (usually 10)
1759
	 * @param int $offset Number of results to skip (usually 0)
1760
	 * @return string[] Matching subpages
1761
	 */
1762
	public function prefixSearchSubpages( $search, $limit, $offset ) {
1763
		return $this->prefixSearchString( $search, $limit, $offset );
1764
	}
1765
1766
	protected function getGroupName() {
1767
		return 'pagetools';
1768
	}
1769
}
1770