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/SpecialMovepage.php (3 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:Movepage
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
 * A special page that allows users to change page titles
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class MovePageForm extends UnlistedSpecialPage {
30
	/** @var Title */
31
	protected $oldTitle = null;
32
33
	/** @var Title */
34
	protected $newTitle;
35
36
	/** @var string Text input */
37
	protected $reason;
38
39
	// Checks
40
41
	/** @var bool */
42
	protected $moveTalk;
43
44
	/** @var bool */
45
	protected $deleteAndMove;
46
47
	/** @var bool */
48
	protected $moveSubpages;
49
50
	/** @var bool */
51
	protected $fixRedirects;
52
53
	/** @var bool */
54
	protected $leaveRedirect;
55
56
	/** @var bool */
57
	protected $moveOverShared;
58
59
	private $watch = false;
60
61
	public function __construct() {
62
		parent::__construct( 'Movepage' );
63
	}
64
65
	public function doesWrites() {
66
		return true;
67
	}
68
69
	public function execute( $par ) {
70
		$this->useTransactionalTimeLimit();
71
72
		$this->checkReadOnly();
73
74
		$this->setHeaders();
75
		$this->outputHeader();
76
77
		$request = $this->getRequest();
78
		$target = !is_null( $par ) ? $par : $request->getVal( 'target' );
79
80
		// Yes, the use of getVal() and getText() is wanted, see bug 20365
81
82
		$oldTitleText = $request->getVal( 'wpOldTitle', $target );
83
		$this->oldTitle = Title::newFromText( $oldTitleText );
84
85
		if ( !$this->oldTitle ) {
86
			// Either oldTitle wasn't passed, or newFromText returned null
87
			throw new ErrorPageError( 'notargettitle', 'notargettext' );
88
		}
89
		if ( !$this->oldTitle->exists() ) {
90
			throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
91
		}
92
93
		$newTitleTextMain = $request->getText( 'wpNewTitleMain' );
94
		$newTitleTextNs = $request->getInt( 'wpNewTitleNs', $this->oldTitle->getNamespace() );
95
		// Backwards compatibility for forms submitting here from other sources
96
		// which is more common than it should be..
97
		$newTitleText_bc = $request->getText( 'wpNewTitle' );
98
		$this->newTitle = strlen( $newTitleText_bc ) > 0
99
			? Title::newFromText( $newTitleText_bc )
100
			: Title::makeTitleSafe( $newTitleTextNs, $newTitleTextMain );
101
102
		$user = $this->getUser();
103
104
		# Check rights
105
		$permErrors = $this->oldTitle->getUserPermissionsErrors( 'move', $user );
106
		if ( count( $permErrors ) ) {
107
			// Auto-block user's IP if the account was "hard" blocked
108
			DeferredUpdates::addCallableUpdate( function() use ( $user ) {
109
				$user->spreadAnyEditBlock();
110
			} );
111
			throw new PermissionsError( 'move', $permErrors );
112
		}
113
114
		$def = !$request->wasPosted();
115
116
		$this->reason = $request->getText( 'wpReason' );
117
		$this->moveTalk = $request->getBool( 'wpMovetalk', $def );
118
		$this->fixRedirects = $request->getBool( 'wpFixRedirects', $def );
119
		$this->leaveRedirect = $request->getBool( 'wpLeaveRedirect', $def );
120
		$this->moveSubpages = $request->getBool( 'wpMovesubpages' );
121
		$this->deleteAndMove = $request->getBool( 'wpDeleteAndMove' );
122
		$this->moveOverShared = $request->getBool( 'wpMoveOverSharedFile' );
123
		$this->watch = $request->getCheck( 'wpWatch' ) && $user->isLoggedIn();
124
125
		if ( 'submit' == $request->getVal( 'action' ) && $request->wasPosted()
126
			&& $user->matchEditToken( $request->getVal( 'wpEditToken' ) )
127
		) {
128
			$this->doSubmit();
129
		} else {
130
			$this->showForm( [] );
131
		}
132
	}
133
134
	/**
135
	 * Show the form
136
	 *
137
	 * @param array $err Error messages. Each item is an error message.
138
	 *    It may either be a string message name or array message name and
139
	 *    parameters, like the second argument to OutputPage::wrapWikiMsg().
140
	 */
141
	function showForm( $err ) {
142
		$this->getSkin()->setRelevantTitle( $this->oldTitle );
143
144
		$out = $this->getOutput();
145
		$out->setPageTitle( $this->msg( 'move-page', $this->oldTitle->getPrefixedText() ) );
146
		$out->addModules( 'mediawiki.special.movePage' );
147
		$out->addModuleStyles( 'mediawiki.special.movePage.styles' );
148
		$this->addHelpLink( 'Help:Moving a page' );
149
150
		$out->addWikiMsg( $this->getConfig()->get( 'FixDoubleRedirects' ) ?
151
			'movepagetext' :
152
			'movepagetext-noredirectfixer'
153
		);
154
155
		if ( $this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) {
156
			$out->wrapWikiMsg(
157
				"<div class=\"warningbox mw-moveuserpage-warning\">\n$1\n</div>",
158
				'moveuserpage-warning'
159
			);
160
		} elseif ( $this->oldTitle->getNamespace() == NS_CATEGORY ) {
161
			$out->wrapWikiMsg(
162
				"<div class=\"warningbox mw-movecategorypage-warning\">\n$1\n</div>",
163
				'movecategorypage-warning'
164
			);
165
		}
166
167
		$deleteAndMove = false;
168
		$moveOverShared = false;
169
170
		$newTitle = $this->newTitle;
171
172
		if ( !$newTitle ) {
173
			# Show the current title as a default
174
			# when the form is first opened.
175
			$newTitle = $this->oldTitle;
176
		} elseif ( !count( $err ) ) {
177
			# If a title was supplied, probably from the move log revert
178
			# link, check for validity. We can then show some diagnostic
179
			# information and save a click.
180
			$newerr = $this->oldTitle->isValidMoveOperation( $newTitle );
181
			if ( is_array( $newerr ) ) {
182
				$err = $newerr;
183
			}
184
		}
185
186
		$user = $this->getUser();
187
188 View Code Duplication
		if ( count( $err ) == 1 && isset( $err[0][0] ) && $err[0][0] == 'articleexists'
189
			&& $newTitle->quickUserCan( 'delete', $user )
190
		) {
191
			$out->wrapWikiMsg(
192
				"<div class='warningbox'>\n$1\n</div>\n",
193
				[ 'delete_and_move_text', $newTitle->getPrefixedText() ]
194
			);
195
			$deleteAndMove = true;
196
			$err = [];
197
		}
198
199 View Code Duplication
		if ( count( $err ) == 1 && isset( $err[0][0] ) && $err[0][0] == 'file-exists-sharedrepo'
200
			&& $user->isAllowed( 'reupload-shared' )
201
		) {
202
			$out->wrapWikiMsg(
203
				"<div class='warningbox'>\n$1\n</div>\n",
204
				[
205
					'move-over-sharedrepo',
206
					$newTitle->getPrefixedText()
207
				]
208
			);
209
			$moveOverShared = true;
210
			$err = [];
211
		}
212
213
		$oldTalk = $this->oldTitle->getTalkPage();
214
		$oldTitleSubpages = $this->oldTitle->hasSubpages();
215
		$oldTitleTalkSubpages = $this->oldTitle->getTalkPage()->hasSubpages();
216
217
		$canMoveSubpage = ( $oldTitleSubpages || $oldTitleTalkSubpages ) &&
218
			!count( $this->oldTitle->getUserPermissionsErrors( 'move-subpages', $user ) );
219
220
		# We also want to be able to move assoc. subpage talk-pages even if base page
221
		# has no associated talk page, so || with $oldTitleTalkSubpages.
222
		$considerTalk = !$this->oldTitle->isTalkPage() &&
223
			( $oldTalk->exists()
224
				|| ( $oldTitleTalkSubpages && $canMoveSubpage ) );
225
226
		$dbr = wfGetDB( DB_REPLICA );
227
		if ( $this->getConfig()->get( 'FixDoubleRedirects' ) ) {
228
			$hasRedirects = $dbr->selectField( 'redirect', '1',
229
				[
230
					'rd_namespace' => $this->oldTitle->getNamespace(),
231
					'rd_title' => $this->oldTitle->getDBkey(),
232
				], __METHOD__ );
233
		} else {
234
			$hasRedirects = false;
235
		}
236
237
		if ( count( $err ) ) {
238
			$out->addHTML( "<div class='errorbox'>\n" );
239
			$action_desc = $this->msg( 'action-move' )->plain();
240
			$out->addWikiMsg( 'permissionserrorstext-withaction', count( $err ), $action_desc );
241
242
			if ( count( $err ) == 1 ) {
243
				$errMsg = $err[0];
244
				$errMsgName = array_shift( $errMsg );
245
246
				if ( $errMsgName == 'hookaborted' ) {
247
					$out->addHTML( "<p>{$errMsg[0]}</p>\n" );
248
				} else {
249
					$out->addWikiMsgArray( $errMsgName, $errMsg );
250
				}
251
			} else {
252
				$errStr = [];
253
254
				foreach ( $err as $errMsg ) {
255
					if ( $errMsg[0] == 'hookaborted' ) {
256
						$errStr[] = $errMsg[1];
257
					} else {
258
						$errMsgName = array_shift( $errMsg );
259
						$errStr[] = $this->msg( $errMsgName, $errMsg )->parse();
260
					}
261
				}
262
263
				$out->addHTML( '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n" );
264
			}
265
			$out->addHTML( "</div>\n" );
266
		}
267
268
		if ( $this->oldTitle->isProtected( 'move' ) ) {
269
			# Is the title semi-protected?
270
			if ( $this->oldTitle->isSemiProtected( 'move' ) ) {
271
				$noticeMsg = 'semiprotectedpagemovewarning';
272
				$classes[] = 'mw-textarea-sprotected';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$classes was never initialized. Although not strictly required by PHP, it is generally a good practice to add $classes = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
273
			} else {
274
				# Then it must be protected based on static groups (regular)
275
				$noticeMsg = 'protectedpagemovewarning';
276
				$classes[] = 'mw-textarea-protected';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$classes was never initialized. Although not strictly required by PHP, it is generally a good practice to add $classes = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
277
			}
278
			$out->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
279
			$out->addWikiMsg( $noticeMsg );
280
			LogEventsList::showLogExtract(
281
				$out,
282
				'protect',
283
				$this->oldTitle,
284
				'',
285
				[ 'lim' => 1 ]
286
			);
287
			$out->addHTML( "</div>\n" );
288
		}
289
290
		// Byte limit (not string length limit) for wpReason and wpNewTitleMain
291
		// is enforced in the mediawiki.special.movePage module
292
293
		$immovableNamespaces = [];
294
		foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
295
			if ( !MWNamespace::isMovable( $nsId ) ) {
296
				$immovableNamespaces[] = $nsId;
297
			}
298
		}
299
300
		$handler = ContentHandler::getForTitle( $this->oldTitle );
301
302
		$out->enableOOUI();
303
		$fields = [];
304
305
		$fields[] = new OOUI\FieldLayout(
306
			new MediaWiki\Widget\ComplexTitleInputWidget( [
307
				'id' => 'wpNewTitle',
308
				'namespace' => [
309
					'id' => 'wpNewTitleNs',
310
					'name' => 'wpNewTitleNs',
311
					'value' => $newTitle->getNamespace(),
312
					'exclude' => $immovableNamespaces,
313
				],
314
				'title' => [
315
					'id' => 'wpNewTitleMain',
316
					'name' => 'wpNewTitleMain',
317
					'value' => $newTitle->getText(),
318
					// Inappropriate, since we're expecting the user to input a non-existent page's title
319
					'suggestions' => false,
320
				],
321
				'infusable' => true,
322
			] ),
323
			[
324
				'label' => $this->msg( 'newtitle' )->text(),
325
				'align' => 'top',
326
			]
327
		);
328
329
		$fields[] = new OOUI\FieldLayout(
330
			new OOUI\TextInputWidget( [
331
				'name' => 'wpReason',
332
				'id' => 'wpReason',
333
				'maxLength' => 200,
334
				'infusable' => true,
335
				'value' => $this->reason,
336
			] ),
337
			[
338
				'label' => $this->msg( 'movereason' )->text(),
339
				'align' => 'top',
340
			]
341
		);
342
343
		if ( $considerTalk ) {
344
			$fields[] = new OOUI\FieldLayout(
345
				new OOUI\CheckboxInputWidget( [
346
					'name' => 'wpMovetalk',
347
					'id' => 'wpMovetalk',
348
					'value' => '1',
349
					'selected' => $this->moveTalk,
350
				] ),
351
				[
352
					'label' => $this->msg( 'movetalk' )->text(),
353
					'help' => new OOUI\HtmlSnippet( $this->msg( 'movepagetalktext' )->parseAsBlock() ),
354
					'align' => 'inline',
355
					'infusable' => true,
356
					'id' => 'wpMovetalk-field',
357
				]
358
			);
359
		}
360
361
		if ( $user->isAllowed( 'suppressredirect' ) ) {
362
			if ( $handler->supportsRedirects() ) {
363
				$isChecked = $this->leaveRedirect;
364
				$isDisabled = false;
365
			} else {
366
				$isChecked = false;
367
				$isDisabled = true;
368
			}
369
			$fields[] = new OOUI\FieldLayout(
370
				new OOUI\CheckboxInputWidget( [
371
					'name' => 'wpLeaveRedirect',
372
					'id' => 'wpLeaveRedirect',
373
					'value' => '1',
374
					'selected' => $isChecked,
375
					'disabled' => $isDisabled,
376
				] ),
377
				[
378
					'label' => $this->msg( 'move-leave-redirect' )->text(),
379
					'align' => 'inline',
380
				]
381
			);
382
		}
383
384 View Code Duplication
		if ( $hasRedirects ) {
385
			$fields[] = new OOUI\FieldLayout(
386
				new OOUI\CheckboxInputWidget( [
387
					'name' => 'wpFixRedirects',
388
					'id' => 'wpFixRedirects',
389
					'value' => '1',
390
					'selected' => $this->fixRedirects,
391
				] ),
392
				[
393
					'label' => $this->msg( 'fix-double-redirects' )->text(),
394
					'align' => 'inline',
395
				]
396
			);
397
		}
398
399
		if ( $canMoveSubpage ) {
400
			$maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' );
401
			$fields[] = new OOUI\FieldLayout(
402
				new OOUI\CheckboxInputWidget( [
403
					'name' => 'wpMovesubpages',
404
					'id' => 'wpMovesubpages',
405
					'value' => '1',
406
					# Don't check the box if we only have talk subpages to
407
					# move and we aren't moving the talk page.
408
					'selected' => $this->moveSubpages && ( $this->oldTitle->hasSubpages() || $this->moveTalk ),
409
				] ),
410
				[
411
					'label' => new OOUI\HtmlSnippet(
412
						$this->msg(
413
							( $this->oldTitle->hasSubpages()
414
								? 'move-subpages'
415
								: 'move-talk-subpages' )
416
						)->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse()
417
					),
418
					'align' => 'inline',
419
				]
420
			);
421
		}
422
423
		# Don't allow watching if user is not logged in
424
		if ( $user->isLoggedIn() ) {
425
			$watchChecked = $user->isLoggedIn() && ( $this->watch || $user->getBoolOption( 'watchmoves' )
426
				|| $user->isWatched( $this->oldTitle ) );
427
			$fields[] = new OOUI\FieldLayout(
428
				new OOUI\CheckboxInputWidget( [
429
					'name' => 'wpWatch',
430
					'id' => 'watch', # ew
431
					'value' => '1',
432
					'selected' => $watchChecked,
433
				] ),
434
				[
435
					'label' => $this->msg( 'move-watch' )->text(),
436
					'align' => 'inline',
437
				]
438
			);
439
		}
440
441
		$hiddenFields = '';
442
		if ( $moveOverShared ) {
443
			$hiddenFields .= Html::hidden( 'wpMoveOverSharedFile', '1' );
444
		}
445
446 View Code Duplication
		if ( $deleteAndMove ) {
447
			$fields[] = new OOUI\FieldLayout(
448
				new OOUI\CheckboxInputWidget( [
449
					'name' => 'wpDeleteAndMove',
450
					'id' => 'wpDeleteAndMove',
451
					'value' => '1',
452
				] ),
453
				[
454
					'label' => $this->msg( 'delete_and_move_confirm' )->text(),
455
					'align' => 'inline',
456
				]
457
			);
458
		}
459
460
		$fields[] = new OOUI\FieldLayout(
461
			new OOUI\ButtonInputWidget( [
462
				'name' => 'wpMove',
463
				'value' => $this->msg( 'movepagebtn' )->text(),
464
				'label' => $this->msg( 'movepagebtn' )->text(),
465
				'flags' => [ 'primary', 'progressive' ],
466
				'type' => 'submit',
467
			] ),
468
			[
469
				'align' => 'top',
470
			]
471
		);
472
473
		$fieldset = new OOUI\FieldsetLayout( [
474
			'label' => $this->msg( 'move-page-legend' )->text(),
475
			'id' => 'mw-movepage-table',
476
			'items' => $fields,
477
		] );
478
479
		$form = new OOUI\FormLayout( [
480
			'method' => 'post',
481
			'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ),
482
			'id' => 'movepage',
483
		] );
484
		$form->appendContent(
485
			$fieldset,
486
			new OOUI\HtmlSnippet(
487
				$hiddenFields .
488
				Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
489
				Html::hidden( 'wpEditToken', $user->getEditToken() )
490
			)
491
		);
492
493
		$out->addHTML(
494
			new OOUI\PanelLayout( [
495
				'classes' => [ 'movepage-wrapper' ],
496
				'expanded' => false,
497
				'padded' => true,
498
				'framed' => true,
499
				'content' => $form,
500
			] )
501
		);
502
503
		$this->showLogFragment( $this->oldTitle );
504
		$this->showSubpages( $this->oldTitle );
505
	}
506
507
	function doSubmit() {
508
		$user = $this->getUser();
509
510
		if ( $user->pingLimiter( 'move' ) ) {
511
			throw new ThrottledError;
512
		}
513
514
		$ot = $this->oldTitle;
515
		$nt = $this->newTitle;
516
517
		# don't allow moving to pages with # in
518
		if ( !$nt || $nt->hasFragment() ) {
519
			$this->showForm( [ [ 'badtitletext' ] ] );
520
521
			return;
522
		}
523
524
		# Show a warning if the target file exists on a shared repo
525
		if ( $nt->getNamespace() == NS_FILE
526
			&& !( $this->moveOverShared && $user->isAllowed( 'reupload-shared' ) )
527
			&& !RepoGroup::singleton()->getLocalRepo()->findFile( $nt )
528
			&& wfFindFile( $nt )
529
		) {
530
			$this->showForm( [ [ 'file-exists-sharedrepo' ] ] );
531
532
			return;
533
		}
534
535
		# Delete to make way if requested
536
		if ( $this->deleteAndMove ) {
537
			$permErrors = $nt->getUserPermissionsErrors( 'delete', $user );
538
			if ( count( $permErrors ) ) {
539
				# Only show the first error
540
				$this->showForm( $permErrors );
541
542
				return;
543
			}
544
545
			$reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
546
547
			// Delete an associated image if there is
548
			if ( $nt->getNamespace() == NS_FILE ) {
549
				$file = wfLocalFile( $nt );
550
				$file->load( File::READ_LATEST );
551
				if ( $file->exists() ) {
552
					$file->delete( $reason, false, $user );
553
				}
554
			}
555
556
			$error = ''; // passed by ref
557
			$page = WikiPage::factory( $nt );
558
			$deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
559
			if ( !$deleteStatus->isGood() ) {
560
				$this->showForm( $deleteStatus->getErrorsArray() );
561
562
				return;
563
			}
564
		}
565
566
		$handler = ContentHandler::getForTitle( $ot );
567
568
		if ( !$handler->supportsRedirects() ) {
569
			$createRedirect = false;
570
		} elseif ( $user->isAllowed( 'suppressredirect' ) ) {
571
			$createRedirect = $this->leaveRedirect;
572
		} else {
573
			$createRedirect = true;
574
		}
575
576
		# Do the actual move.
577
		$mp = new MovePage( $ot, $nt );
578
		$valid = $mp->isValidMove();
579
		if ( !$valid->isOK() ) {
580
			$this->showForm( $valid->getErrorsArray() );
581
			return;
582
		}
583
584
		$permStatus = $mp->checkPermissions( $user, $this->reason );
585
		if ( !$permStatus->isOK() ) {
586
			$this->showForm( $permStatus->getErrorsArray() );
587
			return;
588
		}
589
590
		$status = $mp->move( $user, $this->reason, $createRedirect );
591
		if ( !$status->isOK() ) {
592
			$this->showForm( $status->getErrorsArray() );
593
			return;
594
		}
595
596
		if ( $this->getConfig()->get( 'FixDoubleRedirects' ) && $this->fixRedirects ) {
597
			DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
598
		}
599
600
		$out = $this->getOutput();
601
		$out->setPageTitle( $this->msg( 'pagemovedsub' ) );
602
603
		$linkRenderer = $this->getLinkRenderer();
604
		$oldLink = $linkRenderer->makeLink(
605
			$ot,
606
			null,
607
			[ 'id' => 'movepage-oldlink' ],
608
			[ 'redirect' => 'no' ]
609
		);
610
		$newLink = $linkRenderer->makeKnownLink(
611
			$nt,
612
			null,
613
			[ 'id' => 'movepage-newlink' ]
614
		);
615
		$oldText = $ot->getPrefixedText();
616
		$newText = $nt->getPrefixedText();
617
618
		if ( $ot->exists() ) {
619
			// NOTE: we assume that if the old title exists, it's because it was re-created as
620
			// a redirect to the new title. This is not safe, but what we did before was
621
			// even worse: we just determined whether a redirect should have been created,
622
			// and reported that it was created if it should have, without any checks.
623
			// Also note that isRedirect() is unreliable because of bug 37209.
624
			$msgName = 'movepage-moved-redirect';
625
		} else {
626
			$msgName = 'movepage-moved-noredirect';
627
		}
628
629
		$out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink,
630
			$newLink )->params( $oldText, $newText )->parseAsBlock() );
631
		$out->addWikiMsg( $msgName );
632
633
		Hooks::run( 'SpecialMovepageAfterMove', [ &$this, &$ot, &$nt ] );
634
635
		# Now we move extra pages we've been asked to move: subpages and talk
636
		# pages.  First, if the old page or the new page is a talk page, we
637
		# can't move any talk pages: cancel that.
638
		if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
639
			$this->moveTalk = false;
640
		}
641
642
		if ( count( $ot->getUserPermissionsErrors( 'move-subpages', $user ) ) ) {
643
			$this->moveSubpages = false;
644
		}
645
646
		/**
647
		 * Next make a list of id's.  This might be marginally less efficient
648
		 * than a more direct method, but this is not a highly performance-cri-
649
		 * tical code path and readable code is more important here.
650
		 *
651
		 * If the target namespace doesn't allow subpages, moving with subpages
652
		 * would mean that you couldn't move them back in one operation, which
653
		 * is bad.
654
		 * @todo FIXME: A specific error message should be given in this case.
655
		 */
656
657
		// @todo FIXME: Use Title::moveSubpages() here
658
		$dbr = wfGetDB( DB_MASTER );
659
		if ( $this->moveSubpages && (
660
			MWNamespace::hasSubpages( $nt->getNamespace() ) || (
661
				$this->moveTalk
662
					&& MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
663
			)
664
		) ) {
665
			$conds = [
666
				'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() )
667
					. ' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
668
			];
669
			$conds['page_namespace'] = [];
670
			if ( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
671
				$conds['page_namespace'][] = $ot->getNamespace();
672
			}
673
			if ( $this->moveTalk &&
674
				MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
675
			) {
676
				$conds['page_namespace'][] = $ot->getTalkPage()->getNamespace();
677
			}
678
		} elseif ( $this->moveTalk ) {
679
			$conds = [
680
				'page_namespace' => $ot->getTalkPage()->getNamespace(),
681
				'page_title' => $ot->getDBkey()
682
			];
683
		} else {
684
			# Skip the query
685
			$conds = null;
686
		}
687
688
		$extraPages = [];
689
		if ( !is_null( $conds ) ) {
690
			$extraPages = TitleArray::newFromResult(
691
				$dbr->select( 'page',
692
					[ 'page_id', 'page_namespace', 'page_title' ],
693
					$conds,
694
					__METHOD__
695
				)
696
			);
697
		}
698
699
		$extraOutput = [];
700
		$count = 1;
701
		foreach ( $extraPages as $oldSubpage ) {
0 ignored issues
show
The expression $extraPages of type object<TitleArrayFromResult>|null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
702
			if ( $ot->equals( $oldSubpage ) || $nt->equals( $oldSubpage ) ) {
703
				# Already did this one.
704
				continue;
705
			}
706
707
			$newPageName = preg_replace(
708
				'#^' . preg_quote( $ot->getDBkey(), '#' ) . '#',
709
				StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
710
				$oldSubpage->getDBkey()
711
			);
712
713
			if ( $oldSubpage->isSubpage() && ( $ot->isTalkPage() xor $nt->isTalkPage() ) ) {
714
				// Moving a subpage from a subject namespace to a talk namespace or vice-versa
715
				$newNs = $nt->getNamespace();
716
			} elseif ( $oldSubpage->isTalkPage() ) {
717
				$newNs = $nt->getTalkPage()->getNamespace();
718
			} else {
719
				$newNs = $nt->getSubjectPage()->getNamespace();
720
			}
721
722
			# Bug 14385: we need makeTitleSafe because the new page names may
723
			# be longer than 255 characters.
724
			$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
725
			if ( !$newSubpage ) {
726
				$oldLink = $linkRenderer->makeKnownLink( $oldSubpage );
727
				$extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink )
728
					->params( Title::makeName( $newNs, $newPageName ) )->escaped();
729
				continue;
730
			}
731
732
			# This was copy-pasted from Renameuser, bleh.
733
			if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) {
734
				$link = $linkRenderer->makeKnownLink( $newSubpage );
735
				$extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
736
			} else {
737
				$success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect );
738
739
				if ( $success === true ) {
740
					if ( $this->fixRedirects ) {
741
						DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
742
					}
743
					$oldLink = $linkRenderer->makeLink(
744
						$oldSubpage,
745
						null,
746
						[],
747
						[ 'redirect' => 'no' ]
748
					);
749
750
					$newLink = $linkRenderer->makeKnownLink( $newSubpage );
751
					$extraOutput[] = $this->msg( 'movepage-page-moved' )
752
						->rawParams( $oldLink, $newLink )->escaped();
753
					++$count;
754
755
					$maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' );
756
					if ( $count >= $maximumMovedPages ) {
757
						$extraOutput[] = $this->msg( 'movepage-max-pages' )
758
							->numParams( $maximumMovedPages )->escaped();
759
						break;
760
					}
761
				} else {
762
					$oldLink = $linkRenderer->makeKnownLink( $oldSubpage );
763
					$newLink = $linkRenderer->makeLink( $newSubpage );
764
					$extraOutput[] = $this->msg( 'movepage-page-unmoved' )
765
						->rawParams( $oldLink, $newLink )->escaped();
766
				}
767
			}
768
		}
769
770
		if ( $extraOutput !== [] ) {
771
			$out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
772
		}
773
774
		# Deal with watches (we don't watch subpages)
775
		WatchAction::doWatchOrUnwatch( $this->watch, $ot, $user );
776
		WatchAction::doWatchOrUnwatch( $this->watch, $nt, $user );
777
	}
778
779
	function showLogFragment( $title ) {
780
		$moveLogPage = new LogPage( 'move' );
781
		$out = $this->getOutput();
782
		$out->addHTML( Xml::element( 'h2', null, $moveLogPage->getName()->text() ) );
783
		LogEventsList::showLogExtract( $out, 'move', $title );
784
	}
785
786
	/**
787
	 * Show subpages of the page being moved. Section is not shown if both current
788
	 * namespace does not support subpages and no talk subpages were found.
789
	 *
790
	 * @param Title $title Page being moved.
791
	 */
792
	function showSubpages( $title ) {
793
		$nsHasSubpages = MWNamespace::hasSubpages( $title->getNamespace() );
794
		$subpages = $title->getSubpages();
795
		$count = $subpages instanceof TitleArray ? $subpages->count() : 0;
796
797
		$titleIsTalk = $title->isTalkPage();
798
		$subpagesTalk = $title->getTalkPage()->getSubpages();
799
		$countTalk = $subpagesTalk instanceof TitleArray ? $subpagesTalk->count() : 0;
800
		$totalCount = $count + $countTalk;
801
802
		if ( !$nsHasSubpages && $countTalk == 0 ) {
803
			return;
804
		}
805
806
		$this->getOutput()->wrapWikiMsg(
807
			'== $1 ==',
808
			[ 'movesubpage', ( $titleIsTalk ? $count : $totalCount ) ]
809
		);
810
811
		if ( $nsHasSubpages ) {
812
			$this->showSubpagesList( $subpages, $count, 'movesubpagetext', true );
813
		}
814
815
		if ( !$titleIsTalk && $countTalk > 0 ) {
816
			$this->showSubpagesList( $subpagesTalk, $countTalk, 'movesubpagetalktext' );
817
		}
818
	}
819
820
	function showSubpagesList( $subpages, $pagecount, $wikiMsg, $noSubpageMsg = false ) {
821
		$out = $this->getOutput();
822
823
		# No subpages.
824
		if ( $pagecount == 0 && $noSubpageMsg ) {
825
			$out->addWikiMsg( 'movenosubpage' );
826
			return;
827
		}
828
829
		$out->addWikiMsg( $wikiMsg, $this->getLanguage()->formatNum( $pagecount ) );
830
		$out->addHTML( "<ul>\n" );
831
832
		$linkBatch = new LinkBatch( $subpages );
833
		$linkBatch->setCaller( __METHOD__ );
834
		$linkBatch->execute();
835
		$linkRenderer = $this->getLinkRenderer();
836
837
		foreach ( $subpages as $subpage ) {
838
			$link = $linkRenderer->makeLink( $subpage );
839
			$out->addHTML( "<li>$link</li>\n" );
840
		}
841
		$out->addHTML( "</ul>\n" );
842
	}
843
844
	/**
845
	 * Return an array of subpages beginning with $search that this special page will accept.
846
	 *
847
	 * @param string $search Prefix to search for
848
	 * @param int $limit Maximum number of results to return (usually 10)
849
	 * @param int $offset Number of results to skip (usually 0)
850
	 * @return string[] Matching subpages
851
	 */
852
	public function prefixSearchSubpages( $search, $limit, $offset ) {
853
		return $this->prefixSearchString( $search, $limit, $offset );
854
	}
855
856
	protected function getGroupName() {
857
		return 'pagetools';
858
	}
859
}
860