Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/specials/SpecialUndelete.php (39 issues)

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
 * 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_REPLICA );
67
68
		return self::listPages( $dbr, '' );
0 ignored issues
show
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 66 can be null; however, PageArchive::listPages() 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...
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_REPLICA );
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 );
0 ignored issues
show
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 80 can be null; however, PageArchive::listPages() 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...
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_REPLICA );
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
It seems like $conds defined by array('ar_namespace' => ...his->title->getDBkey()) on line 144 can also be of type array; however, Database::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_REPLICA );
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_REPLICA );
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 ] );
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_REPLICA );
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 ) ) ],
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 ) ) ],
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_REPLICA );
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 );
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_REPLICA );
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 );
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_REPLICA );
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
	 * This also sets Status objects, $this->fileStatus and $this->revisionStatus
358
	 * (depending what operations are attempted).
359
	 *
360
	 * @param array $timestamps Pass an empty array to restore all revisions,
361
	 *   otherwise list the ones to undelete.
362
	 * @param string $comment
363
	 * @param array $fileVersions
364
	 * @param bool $unsuppress
365
	 * @param User $user User performing the action, or null to use $wgUser
366
	 * @param string|string[] $tags Change tags to add to log entry
367
	 *   ($user should be able to add the specified tags before this is called)
368
	 * @return array(number of file revisions restored, number of image revisions
0 ignored issues
show
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...
369
	 *   restored, log message) on success, false on failure.
370
	 */
371
	function undelete( $timestamps, $comment = '', $fileVersions = [],
372
		$unsuppress = false, User $user = null, $tags = null
373
	) {
374
		// If both the set of text revisions and file revisions are empty,
375
		// restore everything. Otherwise, just restore the requested items.
376
		$restoreAll = empty( $timestamps ) && empty( $fileVersions );
377
378
		$restoreText = $restoreAll || !empty( $timestamps );
379
		$restoreFiles = $restoreAll || !empty( $fileVersions );
380
381
		if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
382
			$img = wfLocalFile( $this->title );
383
			$img->load( File::READ_LATEST );
384
			$this->fileStatus = $img->restore( $fileVersions, $unsuppress );
385
			if ( !$this->fileStatus->isOK() ) {
386
				return false;
387
			}
388
			$filesRestored = $this->fileStatus->successCount;
389
		} else {
390
			$filesRestored = 0;
391
		}
392
393
		if ( $restoreText ) {
394
			$this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
395
			if ( !$this->revisionStatus->isOK() ) {
396
				return false;
397
			}
398
399
			$textRestored = $this->revisionStatus->getValue();
400
		} else {
401
			$textRestored = 0;
402
		}
403
404
		// Touch the log!
405
406
		if ( $textRestored && $filesRestored ) {
407
			$reason = wfMessage( 'undeletedrevisions-files' )
408
				->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
409
		} elseif ( $textRestored ) {
410
			$reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
411
				->inContentLanguage()->text();
412
		} elseif ( $filesRestored ) {
413
			$reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
414
				->inContentLanguage()->text();
415
		} else {
416
			wfDebug( "Undelete: nothing undeleted...\n" );
417
418
			return false;
419
		}
420
421
		if ( trim( $comment ) != '' ) {
422
			$reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
423
		}
424
425
		if ( $user === null ) {
426
			global $wgUser;
427
			$user = $wgUser;
428
		}
429
430
		$logEntry = new ManualLogEntry( 'delete', 'restore' );
431
		$logEntry->setPerformer( $user );
432
		$logEntry->setTarget( $this->title );
433
		$logEntry->setComment( $reason );
434
		$logEntry->setTags( $tags );
0 ignored issues
show
It seems like $tags defined by parameter $tags on line 372 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...
435
436
		Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
437
438
		$logid = $logEntry->insert();
439
		$logEntry->publish( $logid );
440
441
		return [ $textRestored, $filesRestored, $reason ];
442
	}
443
444
	/**
445
	 * This is the meaty bit -- It restores archived revisions of the given page
446
	 * to the revision table.
447
	 *
448
	 * @param array $timestamps Pass an empty array to restore all revisions,
449
	 *   otherwise list the ones to undelete.
450
	 * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
451
	 * @param string $comment
452
	 * @throws ReadOnlyError
453
	 * @return Status Status object containing the number of revisions restored on success
454
	 */
455
	private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
456
		if ( wfReadOnly() ) {
457
			throw new ReadOnlyError();
458
		}
459
460
		$dbw = wfGetDB( DB_MASTER );
461
		$dbw->startAtomic( __METHOD__ );
462
463
		$restoreAll = empty( $timestamps );
464
465
		# Does this page already exist? We'll have to update it...
466
		$article = WikiPage::factory( $this->title );
467
		# Load latest data for the current page (bug 31179)
468
		$article->loadPageData( 'fromdbmaster' );
469
		$oldcountable = $article->isCountable();
470
471
		$page = $dbw->selectRow( 'page',
472
			[ 'page_id', 'page_latest' ],
473
			[ 'page_namespace' => $this->title->getNamespace(),
474
				'page_title' => $this->title->getDBkey() ],
475
			__METHOD__,
476
			[ 'FOR UPDATE' ] // lock page
477
		);
478
479
		if ( $page ) {
480
			$makepage = false;
481
			# Page already exists. Import the history, and if necessary
482
			# we'll update the latest revision field in the record.
483
484
			# Get the time span of this page
485
			$previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
486
				[ 'rev_id' => $page->page_latest ],
487
				__METHOD__ );
488
489 View Code Duplication
			if ( $previousTimestamp === false ) {
490
				wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
491
492
				$status = Status::newGood( 0 );
493
				$status->warning( 'undeleterevision-missing' );
494
				$dbw->endAtomic( __METHOD__ );
495
496
				return $status;
497
			}
498
		} else {
499
			# Have to create a new article...
500
			$makepage = true;
501
			$previousTimestamp = 0;
502
		}
503
504
		$oldWhere = [
505
			'ar_namespace' => $this->title->getNamespace(),
506
			'ar_title' => $this->title->getDBkey(),
507
		];
508
		if ( !$restoreAll ) {
509
			$oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
510
		}
511
512
		$fields = [
513
			'ar_id',
514
			'ar_rev_id',
515
			'rev_id',
516
			'ar_text',
517
			'ar_comment',
518
			'ar_user',
519
			'ar_user_text',
520
			'ar_timestamp',
521
			'ar_minor_edit',
522
			'ar_flags',
523
			'ar_text_id',
524
			'ar_deleted',
525
			'ar_page_id',
526
			'ar_len',
527
			'ar_sha1'
528
		];
529
530
		if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
531
			$fields[] = 'ar_content_format';
532
			$fields[] = 'ar_content_model';
533
		}
534
535
		/**
536
		 * Select each archived revision...
537
		 */
538
		$result = $dbw->select(
539
			[ 'archive', 'revision' ],
540
			$fields,
541
			$oldWhere,
542
			__METHOD__,
543
			/* options */
544
			[ 'ORDER BY' => 'ar_timestamp' ],
545
			[ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
546
		);
547
548
		$rev_count = $result->numRows();
549 View Code Duplication
		if ( !$rev_count ) {
550
			wfDebug( __METHOD__ . ": no revisions to restore\n" );
551
552
			$status = Status::newGood( 0 );
553
			$status->warning( "undelete-no-results" );
554
			$dbw->endAtomic( __METHOD__ );
555
556
			return $status;
557
		}
558
559
		// We use ar_id because there can be duplicate ar_rev_id even for the same
560
		// page.  In this case, we may be able to restore the first one.
561
		$restoreFailedArIds = [];
562
563
		// Map rev_id to the ar_id that is allowed to use it.  When checking later,
564
		// if it doesn't match, the current ar_id can not be restored.
565
566
		// Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
567
		// rev_id is taken before we even start the restore).
568
		$allowedRevIdToArIdMap = [];
569
570
		$latestRestorableRow = null;
571
572
		foreach ( $result as $row ) {
573
			if ( $row->ar_rev_id ) {
574
				// rev_id is taken even before we start restoring.
575
				if ( $row->ar_rev_id === $row->rev_id ) {
576
					$restoreFailedArIds[] = $row->ar_id;
577
					$allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
578
				} else {
579
					// rev_id is not taken yet in the DB, but it might be taken
580
					// by a prior revision in the same restore operation. If
581
					// not, we need to reserve it.
582
					if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
583
						$restoreFailedArIds[] = $row->ar_id;
584
					} else {
585
						$allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
586
						$latestRestorableRow = $row;
587
					}
588
				}
589
			} else {
590
				// If ar_rev_id is null, there can't be a collision, and a
591
				// rev_id will be chosen automatically.
592
				$latestRestorableRow = $row;
593
			}
594
		}
595
596
		$result->seek( 0 ); // move back
597
598
		$oldPageId = 0;
599
		if ( $latestRestorableRow !== null ) {
600
			$oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
601
602
			// grab the content to check consistency with global state before restoring the page.
603
			$revision = Revision::newFromArchiveRow( $latestRestorableRow,
604
				[
605
					'title' => $article->getTitle(), // used to derive default content model
606
				]
607
			);
608
			$user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
0 ignored issues
show
It seems like $revision->getUserText(\Revision::RAW) targeting Revision::getUserText() can also be of type boolean; however, User::newFromName() does only seem to accept string, maybe add an additional type check?

This check looks at variables that 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...
609
			$content = $revision->getContent( Revision::RAW );
610
611
			// NOTE: article ID may not be known yet. prepareSave() should not modify the database.
612
			$status = $content->prepareSave( $article, 0, -1, $user );
0 ignored issues
show
It seems like $user defined by \User::newFromName($revi...\Revision::RAW), false) on line 608 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...
It seems like $article defined by \WikiPage::factory($this->title) on line 466 can be null; however, Content::prepareSave() 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...
613
			if ( !$status->isOK() ) {
614
				$dbw->endAtomic( __METHOD__ );
615
616
				return $status;
617
			}
618
		}
619
620
		$newid = false; // newly created page ID
621
		$restored = 0; // number of revisions restored
622
		/** @var Revision $revision */
623
		$revision = null;
624
625
		// If there are no restorable revisions, we can skip most of the steps.
626
		if ( $latestRestorableRow === null ) {
627
			$failedRevisionCount = $rev_count;
628
		} else {
629
			if ( $makepage ) {
630
				// Check the state of the newest to-be version...
631 View Code Duplication
				if ( !$unsuppress
632
					&& ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
633
				) {
634
					$dbw->endAtomic( __METHOD__ );
635
636
					return Status::newFatal( "undeleterevdel" );
637
				}
638
				// Safe to insert now...
639
				$newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
0 ignored issues
show
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 460 can be null; however, WikiPage::insertOn() 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...
640
				if ( $newid === false ) {
641
					// The old ID is reserved; let's pick another
642
					$newid = $article->insertOn( $dbw );
0 ignored issues
show
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 460 can be null; however, WikiPage::insertOn() 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...
643
				}
644
				$pageId = $newid;
645
			} else {
646
				// Check if a deleted revision will become the current revision...
647 View Code Duplication
				if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
648
					// Check the state of the newest to-be version...
649
					if ( !$unsuppress
650
						&& ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
651
					) {
652
						$dbw->endAtomic( __METHOD__ );
653
654
						return Status::newFatal( "undeleterevdel" );
655
					}
656
				}
657
658
				$newid = false;
659
				$pageId = $article->getId();
660
			}
661
662
			foreach ( $result as $row ) {
663
				// Check for key dupes due to needed archive integrity.
664
				if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
665
					continue;
666
				}
667
				// Insert one revision at a time...maintaining deletion status
668
				// unless we are specifically removing all restrictions...
669
				$revision = Revision::newFromArchiveRow( $row,
670
					[
671
						'page' => $pageId,
672
						'title' => $this->title,
673
						'deleted' => $unsuppress ? 0 : $row->ar_deleted
674
					] );
675
676
				$revision->insertOn( $dbw );
0 ignored issues
show
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 460 can be null; however, Revision::insertOn() 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...
677
				$restored++;
678
679
				Hooks::run( 'ArticleRevisionUndeleted',
680
					[ &$this->title, $revision, $row->ar_page_id ] );
681
			}
682
683
			// Now that it's safely stored, take it out of the archive
684
			// Don't delete rows that we failed to restore
685
			$toDeleteConds = $oldWhere;
686
			$failedRevisionCount = count( $restoreFailedArIds );
687
			if ( $failedRevisionCount > 0 ) {
688
				$toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
689
			}
690
691
			$dbw->delete( 'archive',
692
				$toDeleteConds,
693
				__METHOD__ );
694
		}
695
696
		$status = Status::newGood( $restored );
697
698
		if ( $failedRevisionCount > 0 ) {
699
			$status->warning(
700
				wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
701
		}
702
703
		// Was anything restored at all?
704
		if ( $restored ) {
705
			$created = (bool)$newid;
706
			// Attach the latest revision to the page...
707
			$wasnew = $article->updateIfNewerOn( $dbw, $revision );
0 ignored issues
show
It seems like $dbw defined by wfGetDB(DB_MASTER) on line 460 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...
708
			if ( $created || $wasnew ) {
709
				// Update site stats, link tables, etc
710
				$article->doEditUpdates(
711
					$revision,
712
					User::newFromName( $revision->getUserText( Revision::RAW ), false ),
0 ignored issues
show
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...
It seems like $revision->getUserText(\Revision::RAW) targeting Revision::getUserText() can also be of type boolean; however, User::newFromName() does only seem to accept string, maybe add an additional type check?

This check looks at variables that 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...
713
					[
714
						'created' => $created,
715
						'oldcountable' => $oldcountable,
716
						'restored' => true
717
					]
718
				);
719
			}
720
721
			Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
722
			if ( $this->title->getNamespace() == NS_FILE ) {
723
				DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
724
			}
725
		}
726
727
		$dbw->endAtomic( __METHOD__ );
728
729
		return $status;
730
	}
731
732
	/**
733
	 * @return Status
734
	 */
735
	function getFileStatus() {
736
		return $this->fileStatus;
737
	}
738
739
	/**
740
	 * @return Status
741
	 */
742
	function getRevisionStatus() {
743
		return $this->revisionStatus;
744
	}
745
}
746
747
/**
748
 * Special page allowing users with the appropriate permissions to view
749
 * and restore deleted content.
750
 *
751
 * @ingroup SpecialPage
752
 */
753
class SpecialUndelete extends SpecialPage {
754
	private	$mAction;
755
	private	$mTarget;
756
	private	$mTimestamp;
757
	private	$mRestore;
758
	private	$mRevdel;
759
	private	$mInvert;
760
	private	$mFilename;
761
	private	$mTargetTimestamp;
762
	private	$mAllowed;
763
	private	$mCanView;
764
	private	$mComment;
765
	private	$mToken;
766
767
	/** @var Title */
768
	private $mTargetObj;
769
770
	function __construct() {
771
		parent::__construct( 'Undelete', 'deletedhistory' );
772
	}
773
774
	public function doesWrites() {
775
		return true;
776
	}
777
778
	function loadRequest( $par ) {
779
		$request = $this->getRequest();
780
		$user = $this->getUser();
781
782
		$this->mAction = $request->getVal( 'action' );
783 View Code Duplication
		if ( $par !== null && $par !== '' ) {
784
			$this->mTarget = $par;
785
		} else {
786
			$this->mTarget = $request->getVal( 'target' );
787
		}
788
789
		$this->mTargetObj = null;
790
791
		if ( $this->mTarget !== null && $this->mTarget !== '' ) {
792
			$this->mTargetObj = Title::newFromText( $this->mTarget );
793
		}
794
795
		$this->mSearchPrefix = $request->getText( 'prefix' );
0 ignored issues
show
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...
796
		$time = $request->getVal( 'timestamp' );
797
		$this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
798
		$this->mFilename = $request->getVal( 'file' );
799
800
		$posted = $request->wasPosted() &&
801
			$user->matchEditToken( $request->getVal( 'wpEditToken' ) );
802
		$this->mRestore = $request->getCheck( 'restore' ) && $posted;
803
		$this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
804
		$this->mInvert = $request->getCheck( 'invert' ) && $posted;
805
		$this->mPreview = $request->getCheck( 'preview' ) && $posted;
0 ignored issues
show
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...
806
		$this->mDiff = $request->getCheck( 'diff' );
0 ignored issues
show
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...
807
		$this->mDiffOnly = $request->getBool( 'diffonly', $this->getUser()->getOption( 'diffonly' ) );
0 ignored issues
show
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...
808
		$this->mComment = $request->getText( 'wpComment' );
809
		$this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $user->isAllowed( 'suppressrevision' );
0 ignored issues
show
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...
810
		$this->mToken = $request->getVal( 'token' );
811
812
		if ( $this->isAllowed( 'undelete' ) && !$user->isBlocked() ) {
813
			$this->mAllowed = true; // user can restore
814
			$this->mCanView = true; // user can view content
815
		} elseif ( $this->isAllowed( 'deletedtext' ) ) {
816
			$this->mAllowed = false; // user cannot restore
817
			$this->mCanView = true; // user can view content
818
			$this->mRestore = false;
819
		} else { // user can only view the list of revisions
820
			$this->mAllowed = false;
821
			$this->mCanView = false;
822
			$this->mTimestamp = '';
823
			$this->mRestore = false;
824
		}
825
826
		if ( $this->mRestore || $this->mInvert ) {
827
			$timestamps = [];
828
			$this->mFileVersions = [];
0 ignored issues
show
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...
829
			foreach ( $request->getValues() as $key => $val ) {
830
				$matches = [];
831
				if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
832
					array_push( $timestamps, $matches[1] );
833
				}
834
835
				if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
836
					$this->mFileVersions[] = intval( $matches[1] );
837
				}
838
			}
839
			rsort( $timestamps );
840
			$this->mTargetTimestamp = $timestamps;
841
		}
842
	}
843
844
	/**
845
	 * Checks whether a user is allowed the permission for the
846
	 * specific title if one is set.
847
	 *
848
	 * @param string $permission
849
	 * @param User $user
850
	 * @return bool
851
	 */
852
	protected function isAllowed( $permission, User $user = null ) {
853
		$user = $user ?: $this->getUser();
854
		if ( $this->mTargetObj !== null ) {
855
			return $this->mTargetObj->userCan( $permission, $user );
856
		} else {
857
			return $user->isAllowed( $permission );
858
		}
859
	}
860
861
	function userCanExecute( User $user ) {
862
		return $this->isAllowed( $this->mRestriction, $user );
863
	}
864
865
	function execute( $par ) {
866
		$this->useTransactionalTimeLimit();
867
868
		$user = $this->getUser();
869
870
		$this->setHeaders();
871
		$this->outputHeader();
872
873
		$this->loadRequest( $par );
874
		$this->checkPermissions(); // Needs to be after mTargetObj is set
875
876
		$out = $this->getOutput();
877
878
		if ( is_null( $this->mTargetObj ) ) {
879
			$out->addWikiMsg( 'undelete-header' );
880
881
			# Not all users can just browse every deleted page from the list
882
			if ( $user->isAllowed( 'browsearchive' ) ) {
883
				$this->showSearchForm();
884
			}
885
886
			return;
887
		}
888
889
		$this->addHelpLink( 'Help:Undelete' );
890
		if ( $this->mAllowed ) {
891
			$out->setPageTitle( $this->msg( 'undeletepage' ) );
892
		} else {
893
			$out->setPageTitle( $this->msg( 'viewdeletedpage' ) );
894
		}
895
896
		$this->getSkin()->setRelevantTitle( $this->mTargetObj );
897
898
		if ( $this->mTimestamp !== '' ) {
899
			$this->showRevision( $this->mTimestamp );
900
		} elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
901
			$file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
902
			// Check if user is allowed to see this file
903
			if ( !$file->exists() ) {
904
				$out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
905 View Code Duplication
			} elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
906
				if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
907
					throw new PermissionsError( 'suppressrevision' );
908
				} else {
909
					throw new PermissionsError( 'deletedtext' );
910
				}
911
			} elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
912
				$this->showFileConfirmationForm( $this->mFilename );
913
			} else {
914
				$this->showFile( $this->mFilename );
915
			}
916
		} elseif ( $this->mAction === "submit" ) {
917
			if ( $this->mRestore ) {
918
				$this->undelete();
919
			} elseif ( $this->mRevdel ) {
920
				$this->redirectToRevDel();
921
			}
922
923
		} else {
924
			$this->showHistory();
925
		}
926
	}
927
928
	/**
929
	 * Convert submitted form data to format expected by RevisionDelete and
930
	 * redirect the request
931
	 */
932
	private function redirectToRevDel() {
933
		$archive = new PageArchive( $this->mTargetObj );
934
935
		$revisions = [];
936
937
		foreach ( $this->getRequest()->getValues() as $key => $val ) {
938
			$matches = [];
939
			if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
940
				$revisions[ $archive->getRevision( $matches[1] )->getId() ] = 1;
941
			}
942
		}
943
		$query = [
944
			"type" => "revision",
945
			"ids" => $revisions,
946
			"target" => $this->mTargetObj->getPrefixedText()
947
		];
948
		$url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query );
949
		$this->getOutput()->redirect( $url );
950
	}
951
952
	function showSearchForm() {
953
		$out = $this->getOutput();
954
		$out->setPageTitle( $this->msg( 'undelete-search-title' ) );
955
		$out->addHTML(
956
			Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ) .
957
				Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) .
958
				Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
959
				Html::rawElement(
960
					'label',
961
					[ 'for' => 'prefix' ],
962
					$this->msg( 'undelete-search-prefix' )->parse()
963
				) .
964
				Xml::input(
965
					'prefix',
966
					20,
967
					$this->mSearchPrefix,
968
					[ 'id' => 'prefix', 'autofocus' => '' ]
969
				) . ' ' .
970
				Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) .
971
				Xml::closeElement( 'fieldset' ) .
972
				Xml::closeElement( 'form' )
973
		);
974
975
		# List undeletable articles
976
		if ( $this->mSearchPrefix ) {
977
			$result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
978
			$this->showList( $result );
979
		}
980
	}
981
982
	/**
983
	 * Generic list of deleted pages
984
	 *
985
	 * @param ResultWrapper $result
986
	 * @return bool
987
	 */
988
	private function showList( $result ) {
989
		$out = $this->getOutput();
990
991
		if ( $result->numRows() == 0 ) {
992
			$out->addWikiMsg( 'undelete-no-results' );
993
994
			return false;
995
		}
996
997
		$out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
998
999
		$undelete = $this->getPageTitle();
1000
		$out->addHTML( "<ul>\n" );
1001
		foreach ( $result as $row ) {
1002
			$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
1003
			if ( $title !== null ) {
1004
				$item = Linker::linkKnown(
1005
					$undelete,
1006
					htmlspecialchars( $title->getPrefixedText() ),
1007
					[],
1008
					[ 'target' => $title->getPrefixedText() ]
1009
				);
1010
			} else {
1011
				// The title is no longer valid, show as text
1012
				$item = Html::element(
1013
					'span',
1014
					[ 'class' => 'mw-invalidtitle' ],
1015
					Linker::getInvalidTitleDescription(
1016
						$this->getContext(),
1017
						$row->ar_namespace,
1018
						$row->ar_title
1019
					)
1020
				);
1021
			}
1022
			$revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
1023
			$out->addHTML( "<li>{$item} ({$revs})</li>\n" );
1024
		}
1025
		$result->free();
1026
		$out->addHTML( "</ul>\n" );
1027
1028
		return true;
1029
	}
1030
1031
	private function showRevision( $timestamp ) {
1032
		if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
1033
			return;
1034
		}
1035
1036
		$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1037
		if ( !Hooks::run( 'UndeleteForm::showRevision', [ &$archive, $this->mTargetObj ] ) ) {
1038
			return;
1039
		}
1040
		$rev = $archive->getRevision( $timestamp );
1041
1042
		$out = $this->getOutput();
1043
		$user = $this->getUser();
1044
1045
		if ( !$rev ) {
1046
			$out->addWikiMsg( 'undeleterevision-missing' );
1047
1048
			return;
1049
		}
1050
1051
		if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1052 View Code Duplication
			if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1053
				$out->wrapWikiMsg(
1054
					"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
1055
				$rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
1056
					'rev-suppressed-text-permission' : 'rev-deleted-text-permission'
1057
				);
1058
1059
				return;
1060
			}
1061
1062
			$out->wrapWikiMsg(
1063
				"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
1064
				$rev->isDeleted( Revision::DELETED_RESTRICTED ) ?
1065
					'rev-suppressed-text-view' : 'rev-deleted-text-view'
1066
			);
1067
			$out->addHTML( '<br />' );
1068
			// and we are allowed to see...
1069
		}
1070
1071
		if ( $this->mDiff ) {
1072
			$previousRev = $archive->getPreviousRevision( $timestamp );
1073
			if ( $previousRev ) {
1074
				$this->showDiff( $previousRev, $rev );
1075
				if ( $this->mDiffOnly ) {
1076
					return;
1077
				}
1078
1079
				$out->addHTML( '<hr />' );
1080
			} else {
1081
				$out->addWikiMsg( 'undelete-nodiff' );
1082
			}
1083
		}
1084
1085
		$link = Linker::linkKnown(
1086
			$this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
1087
			htmlspecialchars( $this->mTargetObj->getPrefixedText() )
1088
		);
1089
1090
		$lang = $this->getLanguage();
1091
1092
		// date and time are separate parameters to facilitate localisation.
1093
		// $time is kept for backward compat reasons.
1094
		$time = $lang->userTimeAndDate( $timestamp, $user );
1095
		$d = $lang->userDate( $timestamp, $user );
1096
		$t = $lang->userTime( $timestamp, $user );
1097
		$userLink = Linker::revUserTools( $rev );
1098
1099
		$content = $rev->getContent( Revision::FOR_THIS_USER, $user );
1100
1101
		$isText = ( $content instanceof TextContent );
1102
1103
		if ( $this->mPreview || $isText ) {
1104
			$openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
1105
		} else {
1106
			$openDiv = '<div id="mw-undelete-revision">';
1107
		}
1108
		$out->addHTML( $openDiv );
1109
1110
		// Revision delete links
1111
		if ( !$this->mDiff ) {
1112
			$revdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
1113
			if ( $revdel ) {
1114
				$out->addHTML( "$revdel " );
1115
			}
1116
		}
1117
1118
		$out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
1119
			$time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
1120
1121
		if ( !Hooks::run( 'UndeleteShowRevision', [ $this->mTargetObj, $rev ] ) ) {
1122
			return;
1123
		}
1124
1125
		if ( ( $this->mPreview || !$isText ) && $content ) {
1126
			// NOTE: non-text content has no source view, so always use rendered preview
1127
1128
			// Hide [edit]s
1129
			$popts = $out->parserOptions();
1130
			$popts->setEditSection( false );
1131
1132
			$pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
1133
			$out->addParserOutput( $pout );
1134
		}
1135
1136
		if ( $isText ) {
1137
			// source view for textual content
1138
			$sourceView = Xml::element(
1139
				'textarea',
1140
				[
1141
					'readonly' => 'readonly',
1142
					'cols' => $user->getIntOption( 'cols' ),
1143
					'rows' => $user->getIntOption( 'rows' )
1144
				],
1145
				$content->getNativeData() . "\n"
1146
			);
1147
1148
			$previewButton = Xml::element( 'input', [
1149
				'type' => 'submit',
1150
				'name' => 'preview',
1151
				'value' => $this->msg( 'showpreview' )->text()
1152
			] );
1153
		} else {
1154
			$sourceView = '';
1155
			$previewButton = '';
1156
		}
1157
1158
		$diffButton = Xml::element( 'input', [
1159
			'name' => 'diff',
1160
			'type' => 'submit',
1161
			'value' => $this->msg( 'showdiff' )->text() ] );
1162
1163
		$out->addHTML(
1164
			$sourceView .
1165
				Xml::openElement( 'div', [
1166
					'style' => 'clear: both' ] ) .
1167
				Xml::openElement( 'form', [
1168
					'method' => 'post',
1169
					'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
1170
				Xml::element( 'input', [
1171
					'type' => 'hidden',
1172
					'name' => 'target',
1173
					'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
1174
				Xml::element( 'input', [
1175
					'type' => 'hidden',
1176
					'name' => 'timestamp',
1177
					'value' => $timestamp ] ) .
1178
				Xml::element( 'input', [
1179
					'type' => 'hidden',
1180
					'name' => 'wpEditToken',
1181
					'value' => $user->getEditToken() ] ) .
1182
				$previewButton .
1183
				$diffButton .
1184
				Xml::closeElement( 'form' ) .
1185
				Xml::closeElement( 'div' )
1186
		);
1187
	}
1188
1189
	/**
1190
	 * Build a diff display between this and the previous either deleted
1191
	 * or non-deleted edit.
1192
	 *
1193
	 * @param Revision $previousRev
1194
	 * @param Revision $currentRev
1195
	 * @return string HTML
1196
	 */
1197
	function showDiff( $previousRev, $currentRev ) {
1198
		$diffContext = clone $this->getContext();
1199
		$diffContext->setTitle( $currentRev->getTitle() );
0 ignored issues
show
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...
1200
		$diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
0 ignored issues
show
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...
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...
1201
1202
		$diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
1203
		$diffEngine->showDiffStyle();
1204
1205
		$formattedDiff = $diffEngine->generateContentDiffBody(
1206
			$previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
0 ignored issues
show
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...
1207
			$currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
0 ignored issues
show
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...
1208
		);
1209
1210
		$formattedDiff = $diffEngine->addHeader(
1211
			$formattedDiff,
1212
			$this->diffHeader( $previousRev, 'o' ),
1213
			$this->diffHeader( $currentRev, 'n' )
1214
		);
1215
1216
		$this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
1217
	}
1218
1219
	/**
1220
	 * @param Revision $rev
1221
	 * @param string $prefix
1222
	 * @return string
1223
	 */
1224
	private function diffHeader( $rev, $prefix ) {
1225
		$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...
1226
		if ( $isDeleted ) {
1227
			/// @todo FIXME: $rev->getTitle() is null for deleted revs...?
1228
			$targetPage = $this->getPageTitle();
1229
			$targetQuery = [
1230
				'target' => $this->mTargetObj->getPrefixedText(),
1231
				'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() )
1232
			];
1233
		} else {
1234
			/// @todo FIXME: getId() may return non-zero for deleted revs...
1235
			$targetPage = $rev->getTitle();
1236
			$targetQuery = [ 'oldid' => $rev->getId() ];
1237
		}
1238
1239
		// Add show/hide deletion links if available
1240
		$user = $this->getUser();
1241
		$lang = $this->getLanguage();
1242
		$rdel = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
1243
1244
		if ( $rdel ) {
1245
			$rdel = " $rdel";
1246
		}
1247
1248
		$minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
1249
1250
		$tags = wfGetDB( DB_REPLICA )->selectField(
1251
			'tag_summary',
1252
			'ts_tags',
1253
			[ 'ts_rev_id' => $rev->getId() ],
1254
			__METHOD__
1255
		);
1256
		$tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
1257
1258
		// FIXME This is reimplementing DifferenceEngine#getRevisionHeader
1259
		// and partially #showDiffPage, but worse
1260
		return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
1261
			Linker::link(
1262
				$targetPage,
1263
				$this->msg(
1264
					'revisionasof',
1265
					$lang->userTimeAndDate( $rev->getTimestamp(), $user ),
1266
					$lang->userDate( $rev->getTimestamp(), $user ),
1267
					$lang->userTime( $rev->getTimestamp(), $user )
1268
				)->escaped(),
1269
				[],
1270
				$targetQuery
1271
			) .
1272
			'</strong></div>' .
1273
			'<div id="mw-diff-' . $prefix . 'title2">' .
1274
			Linker::revUserTools( $rev ) . '<br />' .
1275
			'</div>' .
1276
			'<div id="mw-diff-' . $prefix . 'title3">' .
1277
			$minor . Linker::revComment( $rev ) . $rdel . '<br />' .
1278
			'</div>' .
1279
			'<div id="mw-diff-' . $prefix . 'title5">' .
1280
			$tagSummary[0] . '<br />' .
1281
			'</div>';
1282
	}
1283
1284
	/**
1285
	 * Show a form confirming whether a tokenless user really wants to see a file
1286
	 * @param string $key
1287
	 */
1288
	private function showFileConfirmationForm( $key ) {
1289
		$out = $this->getOutput();
1290
		$lang = $this->getLanguage();
1291
		$user = $this->getUser();
1292
		$file = new ArchivedFile( $this->mTargetObj, '', $this->mFilename );
1293
		$out->addWikiMsg( 'undelete-show-file-confirm',
1294
			$this->mTargetObj->getText(),
1295
			$lang->userDate( $file->getTimestamp(), $user ),
1296
			$lang->userTime( $file->getTimestamp(), $user ) );
1297
		$out->addHTML(
1298
			Xml::openElement( 'form', [
1299
					'method' => 'POST',
1300
					'action' => $this->getPageTitle()->getLocalURL( [
1301
						'target' => $this->mTarget,
1302
						'file' => $key,
1303
						'token' => $user->getEditToken( $key ),
1304
					] ),
1305
				]
1306
			) .
1307
				Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
1308
				'</form>'
1309
		);
1310
	}
1311
1312
	/**
1313
	 * Show a deleted file version requested by the visitor.
1314
	 * @param string $key
1315
	 */
1316
	private function showFile( $key ) {
1317
		$this->getOutput()->disable();
1318
1319
		# We mustn't allow the output to be CDN cached, otherwise
1320
		# if an admin previews a deleted image, and it's cached, then
1321
		# a user without appropriate permissions can toddle off and
1322
		# nab the image, and CDN will serve it
1323
		$response = $this->getRequest()->response();
1324
		$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
1325
		$response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
1326
		$response->header( 'Pragma: no-cache' );
1327
1328
		$repo = RepoGroup::singleton()->getLocalRepo();
1329
		$path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
1330
		$repo->streamFile( $path );
1331
	}
1332
1333
	protected function showHistory() {
1334
		$this->checkReadOnly();
1335
1336
		$out = $this->getOutput();
1337
		if ( $this->mAllowed ) {
1338
			$out->addModules( 'mediawiki.special.undelete' );
1339
		}
1340
		$out->wrapWikiMsg(
1341
			"<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1342
			[ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1343
		);
1344
1345
		$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1346
		Hooks::run( 'UndeleteForm::showHistory', [ &$archive, $this->mTargetObj ] );
1347
		/*
1348
		$text = $archive->getLastRevisionText();
1349
		if( is_null( $text ) ) {
1350
			$out->addWikiMsg( 'nohistory' );
1351
			return;
1352
		}
1353
		*/
1354
		$out->addHTML( '<div class="mw-undelete-history">' );
1355
		if ( $this->mAllowed ) {
1356
			$out->addWikiMsg( 'undeletehistory' );
1357
			$out->addWikiMsg( 'undeleterevdel' );
1358
		} else {
1359
			$out->addWikiMsg( 'undeletehistorynoadmin' );
1360
		}
1361
		$out->addHTML( '</div>' );
1362
1363
		# List all stored revisions
1364
		$revisions = $archive->listRevisions();
1365
		$files = $archive->listFiles();
1366
1367
		$haveRevisions = $revisions && $revisions->numRows() > 0;
1368
		$haveFiles = $files && $files->numRows() > 0;
1369
1370
		# Batch existence check on user and talk pages
1371 View Code Duplication
		if ( $haveRevisions ) {
1372
			$batch = new LinkBatch();
1373
			foreach ( $revisions as $row ) {
1374
				$batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
0 ignored issues
show
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...
1375
				$batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
0 ignored issues
show
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...
1376
			}
1377
			$batch->execute();
1378
			$revisions->seek( 0 );
1379
		}
1380 View Code Duplication
		if ( $haveFiles ) {
1381
			$batch = new LinkBatch();
1382
			foreach ( $files as $row ) {
1383
				$batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
0 ignored issues
show
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...
1384
				$batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
0 ignored issues
show
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...
1385
			}
1386
			$batch->execute();
1387
			$files->seek( 0 );
1388
		}
1389
1390
		if ( $this->mAllowed ) {
1391
			$action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
1392
			# Start the form here
1393
			$top = Xml::openElement(
1394
				'form',
1395
				[ 'method' => 'post', 'action' => $action, 'id' => 'undelete' ]
1396
			);
1397
			$out->addHTML( $top );
1398
		}
1399
1400
		# Show relevant lines from the deletion log:
1401
		$deleteLogPage = new LogPage( 'delete' );
1402
		$out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
1403
		LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
1404
		# Show relevant lines from the suppression log:
1405
		$suppressLogPage = new LogPage( 'suppress' );
1406
		if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
1407
			$out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
1408
			LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
1409
		}
1410
1411
		if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1412
			# Format the user-visible controls (comment field, submission button)
1413
			# in a nice little table
1414
			if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
1415
				$unsuppressBox =
1416
					"<tr>
1417
						<td>&#160;</td>
1418
						<td class='mw-input'>" .
1419
						Xml::checkLabel( $this->msg( 'revdelete-unsuppress' )->text(),
1420
							'wpUnsuppress', 'mw-undelete-unsuppress', $this->mUnsuppress ) .
1421
						"</td>
1422
					</tr>";
1423
			} else {
1424
				$unsuppressBox = '';
1425
			}
1426
1427
			$table = Xml::fieldset( $this->msg( 'undelete-fieldset-title' )->text() ) .
1428
				Xml::openElement( 'table', [ 'id' => 'mw-undelete-table' ] ) .
1429
				"<tr>
1430
					<td colspan='2' class='mw-undelete-extrahelp'>" .
1431
				$this->msg( 'undeleteextrahelp' )->parseAsBlock() .
1432
				"</td>
1433
			</tr>
1434
			<tr>
1435
				<td class='mw-label'>" .
1436
				Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) .
1437
				"</td>
1438
				<td class='mw-input'>" .
1439
				Xml::input(
1440
					'wpComment',
1441
					50,
1442
					$this->mComment,
1443
					[ 'id' => 'wpComment', 'autofocus' => '' ]
1444
				) .
1445
				"</td>
1446
			</tr>
1447
			<tr>
1448
				<td>&#160;</td>
1449
				<td class='mw-submit'>" .
1450
				Xml::submitButton(
1451
					$this->msg( 'undeletebtn' )->text(),
1452
					[ 'name' => 'restore', 'id' => 'mw-undelete-submit' ]
1453
				) . ' ' .
1454
				Xml::submitButton(
1455
					$this->msg( 'undeleteinvert' )->text(),
1456
					[ 'name' => 'invert', 'id' => 'mw-undelete-invert' ]
1457
				) .
1458
				"</td>
1459
			</tr>" .
1460
				$unsuppressBox .
1461
				Xml::closeElement( 'table' ) .
1462
				Xml::closeElement( 'fieldset' );
1463
1464
			$out->addHTML( $table );
1465
		}
1466
1467
		$out->addHTML( Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n" );
1468
1469
		if ( $haveRevisions ) {
1470
			# Show the page's stored (deleted) history
1471
1472
			if ( $this->getUser()->isAllowed( 'deleterevision' ) ) {
1473
				$out->addHTML( Html::element(
1474
					'button',
1475
					[
1476
						'name' => 'revdel',
1477
						'type' => 'submit',
1478
						'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
1479
					],
1480
					$this->msg( 'showhideselectedversions' )->text()
1481
				) . "\n" );
1482
			}
1483
1484
			$out->addHTML( '<ul>' );
1485
			$remaining = $revisions->numRows();
1486
			$earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
1487
1488
			foreach ( $revisions as $row ) {
1489
				$remaining--;
1490
				$out->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining ) );
1491
			}
1492
			$revisions->free();
1493
			$out->addHTML( '</ul>' );
1494
		} else {
1495
			$out->addWikiMsg( 'nohistory' );
1496
		}
1497
1498
		if ( $haveFiles ) {
1499
			$out->addHTML( Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n" );
1500
			$out->addHTML( '<ul>' );
1501
			foreach ( $files as $row ) {
1502
				$out->addHTML( $this->formatFileRow( $row ) );
1503
			}
1504
			$files->free();
1505
			$out->addHTML( '</ul>' );
1506
		}
1507
1508
		if ( $this->mAllowed ) {
1509
			# Slip in the hidden controls here
1510
			$misc = Html::hidden( 'target', $this->mTarget );
1511
			$misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
1512
			$misc .= Xml::closeElement( 'form' );
1513
			$out->addHTML( $misc );
1514
		}
1515
1516
		return true;
1517
	}
1518
1519
	protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
1520
		$rev = Revision::newFromArchiveRow( $row,
1521
			[
1522
				'title' => $this->mTargetObj
1523
			] );
1524
1525
		$revTextSize = '';
1526
		$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
1527
		// Build checkboxen...
1528
		if ( $this->mAllowed ) {
1529
			if ( $this->mInvert ) {
1530
				if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1531
					$checkBox = Xml::check( "ts$ts" );
1532
				} else {
1533
					$checkBox = Xml::check( "ts$ts", true );
1534
				}
1535
			} else {
1536
				$checkBox = Xml::check( "ts$ts" );
1537
			}
1538
		} else {
1539
			$checkBox = '';
1540
		}
1541
1542
		// Build page & diff links...
1543
		$user = $this->getUser();
1544
		if ( $this->mCanView ) {
1545
			$titleObj = $this->getPageTitle();
1546
			# Last link
1547
			if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
1548
				$pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1549
				$last = $this->msg( 'diff' )->escaped();
1550
			} elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1551
				$pageLink = $this->getPageLink( $rev, $titleObj, $ts );
0 ignored issues
show
It seems like $ts defined by wfTimestamp(TS_MW, $row->ar_timestamp) on line 1526 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...
1552
				$last = Linker::linkKnown(
1553
					$titleObj,
1554
					$this->msg( 'diff' )->escaped(),
1555
					[],
1556
					[
1557
						'target' => $this->mTargetObj->getPrefixedText(),
1558
						'timestamp' => $ts,
1559
						'diff' => 'prev'
1560
					]
1561
				);
1562
			} else {
1563
				$pageLink = $this->getPageLink( $rev, $titleObj, $ts );
0 ignored issues
show
It seems like $ts defined by wfTimestamp(TS_MW, $row->ar_timestamp) on line 1526 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...
1564
				$last = $this->msg( 'diff' )->escaped();
1565
			}
1566
		} else {
1567
			$pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1568
			$last = $this->msg( 'diff' )->escaped();
1569
		}
1570
1571
		// User links
1572
		$userLink = Linker::revUserTools( $rev );
1573
1574
		// Minor edit
1575
		$minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : '';
1576
1577
		// Revision text size
1578
		$size = $row->ar_len;
1579
		if ( !is_null( $size ) ) {
1580
			$revTextSize = Linker::formatRevisionSize( $size );
1581
		}
1582
1583
		// Edit summary
1584
		$comment = Linker::revComment( $rev );
1585
1586
		// Tags
1587
		$attribs = [];
1588
		list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow(
1589
			$row->ts_tags,
1590
			'deletedhistory',
1591
			$this->getContext()
1592
		);
1593
		if ( $classes ) {
1594
			$attribs['class'] = implode( ' ', $classes );
1595
		}
1596
1597
		$revisionRow = $this->msg( 'undelete-revision-row2' )
1598
			->rawParams(
1599
				$checkBox,
1600
				$last,
1601
				$pageLink,
1602
				$userLink,
1603
				$minor,
1604
				$revTextSize,
1605
				$comment,
1606
				$tagSummary
1607
			)
1608
			->escaped();
1609
1610
		return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
1611
	}
1612
1613
	private function formatFileRow( $row ) {
1614
		$file = ArchivedFile::newFromRow( $row );
1615
		$ts = wfTimestamp( TS_MW, $row->fa_timestamp );
1616
		$user = $this->getUser();
1617
1618
		$checkBox = '';
1619
		if ( $this->mCanView && $row->fa_storage_key ) {
1620
			if ( $this->mAllowed ) {
1621
				$checkBox = Xml::check( 'fileid' . $row->fa_id );
1622
			}
1623
			$key = urlencode( $row->fa_storage_key );
1624
			$pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
0 ignored issues
show
It seems like $ts defined by wfTimestamp(TS_MW, $row->fa_timestamp) on line 1615 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...
1625
		} else {
1626
			$pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user );
1627
		}
1628
		$userLink = $this->getFileUser( $file );
1629
		$data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1630
		$bytes = $this->msg( 'parentheses' )
1631
			->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
1632
			->plain();
1633
		$data = htmlspecialchars( $data . ' ' . $bytes );
1634
		$comment = $this->getFileComment( $file );
1635
1636
		// Add show/hide deletion links if available
1637
		$canHide = $this->isAllowed( 'deleterevision' );
1638
		if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1639
			if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1640
				// Revision was hidden from sysops
1641
				$revdlink = Linker::revDeleteLinkDisabled( $canHide );
1642
			} else {
1643
				$query = [
1644
					'type' => 'filearchive',
1645
					'target' => $this->mTargetObj->getPrefixedDBkey(),
1646
					'ids' => $row->fa_id
1647
				];
1648
				$revdlink = Linker::revDeleteLink( $query,
1649
					$file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1650
			}
1651
		} else {
1652
			$revdlink = '';
1653
		}
1654
1655
		return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1656
	}
1657
1658
	/**
1659
	 * Fetch revision text link if it's available to all users
1660
	 *
1661
	 * @param Revision $rev
1662
	 * @param Title $titleObj
1663
	 * @param string $ts Timestamp
1664
	 * @return string
1665
	 */
1666 View Code Duplication
	function getPageLink( $rev, $titleObj, $ts ) {
1667
		$user = $this->getUser();
1668
		$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1669
1670
		if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1671
			return '<span class="history-deleted">' . $time . '</span>';
1672
		}
1673
1674
		$link = Linker::linkKnown(
1675
			$titleObj,
1676
			htmlspecialchars( $time ),
1677
			[],
1678
			[
1679
				'target' => $this->mTargetObj->getPrefixedText(),
1680
				'timestamp' => $ts
1681
			]
1682
		);
1683
1684
		if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1685
			$link = '<span class="history-deleted">' . $link . '</span>';
1686
		}
1687
1688
		return $link;
1689
	}
1690
1691
	/**
1692
	 * Fetch image view link if it's available to all users
1693
	 *
1694
	 * @param File|ArchivedFile $file
1695
	 * @param Title $titleObj
1696
	 * @param string $ts A timestamp
1697
	 * @param string $key A storage key
1698
	 *
1699
	 * @return string HTML fragment
1700
	 */
1701 View Code Duplication
	function getFileLink( $file, $titleObj, $ts, $key ) {
1702
		$user = $this->getUser();
1703
		$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1704
1705
		if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1706
			return '<span class="history-deleted">' . $time . '</span>';
1707
		}
1708
1709
		$link = Linker::linkKnown(
1710
			$titleObj,
1711
			htmlspecialchars( $time ),
1712
			[],
1713
			[
1714
				'target' => $this->mTargetObj->getPrefixedText(),
1715
				'file' => $key,
1716
				'token' => $user->getEditToken( $key )
1717
			]
1718
		);
1719
1720
		if ( $file->isDeleted( File::DELETED_FILE ) ) {
1721
			$link = '<span class="history-deleted">' . $link . '</span>';
1722
		}
1723
1724
		return $link;
1725
	}
1726
1727
	/**
1728
	 * Fetch file's user id if it's available to this user
1729
	 *
1730
	 * @param File|ArchivedFile $file
1731
	 * @return string HTML fragment
1732
	 */
1733
	function getFileUser( $file ) {
1734
		if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) {
1735
			return '<span class="history-deleted">' .
1736
				$this->msg( 'rev-deleted-user' )->escaped() .
1737
				'</span>';
1738
		}
1739
1740
		$link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) .
0 ignored issues
show
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...
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...
1741
			Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() );
1742
1743
		if ( $file->isDeleted( File::DELETED_USER ) ) {
1744
			$link = '<span class="history-deleted">' . $link . '</span>';
1745
		}
1746
1747
		return $link;
1748
	}
1749
1750
	/**
1751
	 * Fetch file upload comment if it's available to this user
1752
	 *
1753
	 * @param File|ArchivedFile $file
1754
	 * @return string HTML fragment
1755
	 */
1756
	function getFileComment( $file ) {
1757
		if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) {
1758
			return '<span class="history-deleted"><span class="comment">' .
1759
				$this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>';
1760
		}
1761
1762
		$link = Linker::commentBlock( $file->getRawDescription() );
0 ignored issues
show
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...
1763
1764
		if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1765
			$link = '<span class="history-deleted">' . $link . '</span>';
1766
		}
1767
1768
		return $link;
1769
	}
1770
1771
	function undelete() {
1772
		if ( $this->getConfig()->get( 'UploadMaintenance' )
1773
			&& $this->mTargetObj->getNamespace() == NS_FILE
1774
		) {
1775
			throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1776
		}
1777
1778
		$this->checkReadOnly();
1779
1780
		$out = $this->getOutput();
1781
		$archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
1782
		Hooks::run( 'UndeleteForm::undelete', [ &$archive, $this->mTargetObj ] );
1783
		$ok = $archive->undelete(
1784
			$this->mTargetTimestamp,
1785
			$this->mComment,
1786
			$this->mFileVersions,
1787
			$this->mUnsuppress,
1788
			$this->getUser()
1789
		);
1790
1791
		if ( is_array( $ok ) ) {
1792
			if ( $ok[1] ) { // Undeleted file count
1793
				Hooks::run( 'FileUndeleteComplete', [
1794
					$this->mTargetObj, $this->mFileVersions,
1795
					$this->getUser(), $this->mComment ] );
1796
			}
1797
1798
			$link = Linker::linkKnown( $this->mTargetObj );
1799
			$out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
1800
		} else {
1801
			$out->setPageTitle( $this->msg( 'undelete-error' ) );
1802
		}
1803
1804
		// Show revision undeletion warnings and errors
1805
		$status = $archive->getRevisionStatus();
1806
		if ( $status && !$status->isGood() ) {
1807
			$out->addWikiText( '<div class="error">' .
1808
				$status->getWikiText(
1809
					'cannotundelete',
1810
					'cannotundelete'
1811
				) . '</div>'
1812
			);
1813
		}
1814
1815
		// Show file undeletion warnings and errors
1816
		$status = $archive->getFileStatus();
1817
		if ( $status && !$status->isGood() ) {
1818
			$out->addWikiText( '<div class="error">' .
1819
				$status->getWikiText(
1820
					'undelete-error-short',
1821
					'undelete-error-long'
1822
				) . '</div>'
1823
			);
1824
		}
1825
	}
1826
1827
	/**
1828
	 * Return an array of subpages beginning with $search that this special page will accept.
1829
	 *
1830
	 * @param string $search Prefix to search for
1831
	 * @param int $limit Maximum number of results to return (usually 10)
1832
	 * @param int $offset Number of results to skip (usually 0)
1833
	 * @return string[] Matching subpages
1834
	 */
1835
	public function prefixSearchSubpages( $search, $limit, $offset ) {
1836
		return $this->prefixSearchString( $search, $limit, $offset );
1837
	}
1838
1839
	protected function getGroupName() {
1840
		return 'pagetools';
1841
	}
1842
}
1843