MediaWiki master
SpecialMovePage.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
41use OOUI\ButtonInputWidget;
42use OOUI\CheckboxInputWidget;
43use OOUI\DropdownInputWidget;
44use OOUI\FieldLayout;
45use OOUI\FieldsetLayout;
46use OOUI\FormLayout;
47use OOUI\HtmlSnippet;
48use OOUI\PanelLayout;
49use OOUI\TextInputWidget;
50use StatusValue;
54use Wikimedia\Timestamp\TimestampFormat as TS;
55
63 protected $oldTitle = null;
64
66 protected $newTitle;
67
69 protected $reason;
70
72 protected $moveTalk;
73
75 protected $deleteAndMove;
76
78 protected $moveSubpages;
79
81 protected $fixRedirects;
82
84 protected $leaveRedirect;
85
87 protected $moveOverShared;
88
89 private bool $moveOverProtection;
90
92 private $watch = false;
93
94 public function __construct(
95 private readonly MovePageFactory $movePageFactory,
96 private readonly PermissionManager $permManager,
97 private readonly UserOptionsLookup $userOptionsLookup,
98 private readonly IConnectionProvider $dbProvider,
99 private readonly IContentHandlerFactory $contentHandlerFactory,
100 private readonly NamespaceInfo $nsInfo,
101 private readonly LinkBatchFactory $linkBatchFactory,
102 private readonly RepoGroup $repoGroup,
103 private readonly WikiPageFactory $wikiPageFactory,
104 private readonly SearchEngineFactory $searchEngineFactory,
105 private readonly WatchlistManager $watchlistManager,
106 private readonly WatchedItemStore $watchedItemStore,
107 private readonly RestrictionStore $restrictionStore,
108 private readonly TitleFactory $titleFactory,
109 private readonly DeletePageFactory $deletePageFactory,
110 ) {
111 parent::__construct( 'Movepage' );
112 }
113
115 public function doesWrites() {
116 return true;
117 }
118
120 public function execute( $par ) {
122 $this->checkReadOnly();
123 $this->setHeaders();
124 $this->outputHeader();
125
126 $request = $this->getRequest();
127
128 // Beware: The use of WebRequest::getText() is wanted! See T22365
129 $target = $par ?? $request->getText( 'target' );
130 $oldTitleText = $request->getText( 'wpOldTitle', $target );
131 $this->oldTitle = Title::newFromText( $oldTitleText );
132
133 if ( !$this->oldTitle ) {
134 // Either oldTitle wasn't passed, or newFromText returned null
135 throw new ErrorPageError( 'notargettitle', 'notargettext' );
136 }
137 $this->getOutput()->addBacklinkSubtitle( $this->oldTitle );
138 // Various uses of Html::errorBox and Html::warningBox.
139 $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
140
141 if ( !$this->oldTitle->exists() ) {
142 throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
143 }
144
145 $newTitleTextMain = $request->getText( 'wpNewTitleMain' );
146 $newTitleTextNs = $request->getInt( 'wpNewTitleNs', $this->oldTitle->getNamespace() );
147 // Backwards compatibility for forms submitting here from other sources
148 // which is more common than it should be.
149 $newTitleText_bc = $request->getText( 'wpNewTitle' );
150 $this->newTitle = strlen( $newTitleText_bc ) > 0
151 ? Title::newFromText( $newTitleText_bc )
152 : Title::makeTitleSafe( $newTitleTextNs, $newTitleTextMain );
153
154 $user = $this->getUser();
155 $isSubmit = $request->getRawVal( 'action' ) === 'submit' && $request->wasPosted();
156
157 $reasonList = $request->getText( 'wpReasonList', 'other' );
158 $reason = $request->getText( 'wpReason' );
159 if ( $reasonList === 'other' ) {
160 $this->reason = $reason;
161 } elseif ( $reason !== '' ) {
162 $this->reason = $reasonList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reason;
163 } else {
164 $this->reason = $reasonList;
165 }
166 // Default to checked, but don't fill in true during submission (browsers only submit checked values)
167 // TODO: Use HTMLForm to take care of this.
168 $def = !$isSubmit;
169 $this->moveTalk = $request->getBool( 'wpMovetalk', $def );
170 $this->fixRedirects = $request->getBool( 'wpFixRedirects', $def );
171 $this->leaveRedirect = $request->getBool( 'wpLeaveRedirect', $def );
172 // T222953: Tick the "move subpages" box by default
173 $this->moveSubpages = $request->getBool( 'wpMovesubpages', $def );
174 $this->deleteAndMove = $request->getBool( 'wpDeleteAndMove' );
175 $this->moveOverShared = $request->getBool( 'wpMoveOverSharedFile' );
176 $this->moveOverProtection = $request->getBool( 'wpMoveOverProtection' );
177 $this->watch = $request->getCheck( 'wpWatch' ) && $user->isRegistered();
178
179 // Similar to other SpecialPage/Action classes, when tokens fail (likely due to reset or expiry),
180 // do not show an error but show the form again for easy re-submit.
181 if ( $isSubmit && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
182 // Check rights
183 $permStatus = $this->permManager->getPermissionStatus( 'move', $user, $this->oldTitle,
184 PermissionManager::RIGOR_SECURE );
185 // If the account is "hard" blocked, auto-block IP
186 $user->scheduleSpreadBlock();
187 if ( !$permStatus->isGood() ) {
188 throw new PermissionsError( 'move', $permStatus );
189 }
190 $this->doSubmit();
191 } else {
192 // Avoid primary DB connection on form view (T283265)
193 $permStatus = $this->permManager->getPermissionStatus( 'move', $user, $this->oldTitle,
194 PermissionManager::RIGOR_FULL );
195 if ( !$permStatus->isGood() ) {
196 $user->scheduleSpreadBlock();
197 throw new PermissionsError( 'move', $permStatus );
198 }
199 $this->showForm();
200 }
201 }
202
210 private function showForm( ?StatusValue $status = null, ?StatusValue $talkStatus = null ) {
211 $this->getSkin()->setRelevantTitle( $this->oldTitle );
212
213 $out = $this->getOutput();
214 $out->setPageTitleMsg( $this->msg( 'move-page' )->plaintextParams( $this->oldTitle->getPrefixedText() ) );
215 $out->addModuleStyles( [
216 'mediawiki.special',
217 'mediawiki.interface.helpers.styles'
218 ] );
219 $out->addModules( 'mediawiki.misc-authed-ooui' );
220 $this->addHelpLink( 'Help:Moving a page' );
221
222 $handler = $this->contentHandlerFactory
223 ->getContentHandler( $this->oldTitle->getContentModel() );
224 $createRedirect = $handler->supportsRedirects() && !(
225 // Do not create redirects for wikitext message overrides (T376399).
226 // Maybe one day they will have a custom content model and this special case won't be needed.
227 $this->oldTitle->getNamespace() === NS_MEDIAWIKI &&
228 $this->oldTitle->getContentModel() === CONTENT_MODEL_WIKITEXT
229 );
230
231 if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) {
232 $out->addWikiMsg( 'movepagetext' );
233 } else {
234 $out->addWikiMsg( $createRedirect ?
235 'movepagetext-noredirectfixer' :
236 'movepagetext-noredirectsupport' );
237 }
238
239 if ( $this->oldTitle->getNamespace() === NS_USER && !$this->oldTitle->isSubpage() ) {
240 $out->addHTML(
241 Html::warningBox(
242 $out->msg( 'moveuserpage-warning' )->parse(),
243 'mw-moveuserpage-warning'
244 )
245 );
246 // Deselect moveTalk unless it's explicitly given
247 $this->moveTalk = $this->getRequest()->getBool( "wpMovetalk", false );
248 } elseif ( $this->oldTitle->getNamespace() === NS_CATEGORY ) {
249 $out->addHTML(
250 Html::warningBox(
251 $out->msg( 'movecategorypage-warning' )->parse(),
252 'mw-movecategorypage-warning'
253 )
254 );
255 }
256
257 $deleteAndMove = [];
258 $moveOverShared = false;
259
260 $user = $this->getUser();
261 $newTitle = $this->newTitle;
262 $oldTalk = $this->oldTitle->getTalkPageIfDefined();
263
264 if ( !$newTitle ) {
265 # Show the current title as a default
266 # when the form is first opened.
268 } elseif ( !$status ) {
269 # If a title was supplied, probably from the move log revert
270 # link, check for validity. We can then show some diagnostic
271 # information and save a click.
272 $mp = $this->movePageFactory->newMovePage( $this->oldTitle, $newTitle );
273 $status = $mp->isValidMove();
274 $status->merge( $mp->probablyCanMove( $this->getAuthority() ) );
275 if ( $this->moveTalk ) {
276 $newTalk = $newTitle->getTalkPageIfDefined();
277 if ( $oldTalk && $newTalk && $oldTalk->exists() ) {
278 $mpTalk = $this->movePageFactory->newMovePage( $oldTalk, $newTalk );
279 $talkStatus = $mpTalk->isValidMove();
280 $talkStatus->merge( $mpTalk->probablyCanMove( $this->getAuthority() ) );
281 }
282 }
283 }
284 if ( !$status ) {
285 // Caller (execute) is responsible for checking that you have permission to move the page somewhere
286 $status = StatusValue::newGood();
287 }
288 if ( !$talkStatus ) {
289 if ( $oldTalk ) {
290 // If you don't have permission to move the talk page anywhere then complain about that now
291 // rather than only after submitting the form to move the page
292 $talkStatus = $this->permManager->getPermissionStatus( 'move', $user, $oldTalk,
293 PermissionManager::RIGOR_QUICK );
294 } else {
295 // If there's no talk page to move (for example the old page is in a namespace with no talk page)
296 // then this needs to be set to something ...
297 $talkStatus = StatusValue::newGood();
298 }
299 }
300
301 $oldTalk = $this->oldTitle->getTalkPageIfDefined();
302 $oldTitleSubpages = $this->oldTitle->hasSubpages();
303 $oldTitleTalkSubpages = $this->oldTitle->getTalkPageIfDefined()->hasSubpages();
304
305 $canMoveSubpage = ( $oldTitleSubpages || $oldTitleTalkSubpages ) &&
306 $this->permManager->quickUserCan(
307 'move-subpages',
308 $user,
309 $this->oldTitle
310 );
311 # We also want to be able to move assoc. subpage talk-pages even if base page
312 # has no associated talk page, so || with $oldTitleTalkSubpages.
313 $considerTalk = !$this->oldTitle->isTalkPage() &&
314 ( $oldTalk->exists()
315 || ( $oldTitleTalkSubpages && $canMoveSubpage ) );
316
317 if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) {
318 $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
319 ->select( '1' )
320 ->from( 'redirect' )
321 ->where( [ 'rd_namespace' => $this->oldTitle->getNamespace() ] )
322 ->andWhere( [ 'rd_title' => $this->oldTitle->getDBkey() ] )
323 ->andWhere( [ 'rd_interwiki' => '' ] );
324
325 $hasRedirects = (bool)$queryBuilder->caller( __METHOD__ )->fetchField();
326 } else {
327 $hasRedirects = false;
328 }
329
330 $newTalkTitle = $newTitle->getTalkPageIfDefined();
331 $talkOK = $talkStatus->isOK();
332 $mainOK = $status->isOK();
333 $talkIsArticle = $talkIsRedirect = $mainIsArticle = $mainIsRedirect = false;
334 if ( count( $status->getMessages() ) == 1 ) {
335 $mainIsArticle = $status->hasMessage( 'articleexists' )
336 && $this->permManager->quickUserCan( 'delete', $user, $newTitle );
337 $mainIsRedirect = $status->hasMessage( 'redirectexists' ) && (
338 // Any user that can delete normally can also delete a redirect here
339 $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) ||
340 $this->permManager->quickUserCan( 'delete', $user, $newTitle ) );
341 if ( $status->hasMessage( 'file-exists-sharedrepo' )
342 && $this->permManager->userHasRight( $user, 'reupload-shared' )
343 ) {
344 $out->addHTML(
345 Html::warningBox(
346 $out->msg( 'move-over-sharedrepo', $newTitle->getPrefixedText() )->parse()
347 )
348 );
349 $moveOverShared = true;
350 $status = StatusValue::newGood();
351 }
352 }
353 if ( count( $talkStatus->getMessages() ) == 1 ) {
354 $talkIsArticle = $talkStatus->hasMessage( 'articleexists' )
355 && $this->permManager->quickUserCan( 'delete', $user, $newTitle );
356 $talkIsRedirect = $talkStatus->hasMessage( 'redirectexists' ) && (
357 // Any user that can delete normally can also delete a redirect here
358 $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) ||
359 $this->permManager->quickUserCan( 'delete', $user, $newTitle ) );
360 // Talk page is by definition not a file so can't be shared
361 }
362 $warning = null;
363 // Case 1: Two pages need deletions of full history
364 // Either both are articles or one is an article and one is a redirect
365 if ( ( $talkIsArticle && $mainIsArticle ) ||
366 ( $talkIsArticle && $mainIsRedirect ) ||
367 ( $talkIsRedirect && $mainIsArticle )
368 ) {
369 $warning = $out->msg( 'delete_and_move_text_2',
371 $newTalkTitle->getPrefixedText()
372 );
373 $deleteAndMove = [ $newTitle, $newTalkTitle ];
374 // Case 2: Both need simple deletes
375 } elseif ( $mainIsRedirect && $talkIsRedirect ) {
376 $warning = $out->msg( 'delete_redirect_and_move_text_2',
378 $newTalkTitle->getPrefixedText()
379 );
380 $deleteAndMove = [ $newTitle, $newTalkTitle ];
381 // Case 3: The main page needs a full delete, the talk doesn't exist
382 // (or is a single-rev redirect to the source we can silently ignore)
383 } elseif ( $mainIsArticle && $talkOK ) {
384 $warning = $out->msg( 'delete_and_move_text', $newTitle->getPrefixedText() );
386 // Case 4: The main page needs a simple delete, the talk doesn't exist
387 } elseif ( $mainIsRedirect && $talkOK ) {
388 $warning = $out->msg( 'delete_redirect_and_move_text', $newTitle->getPrefixedText() );
390 // Cases 5 and 6: The same for the talk page
391 } elseif ( $talkIsArticle && $mainOK ) {
392 $warning = $out->msg( 'delete_and_move_text', $newTalkTitle->getPrefixedText() );
393 $deleteAndMove = [ $newTalkTitle ];
394 } elseif ( $talkIsRedirect && $mainOK ) {
395 $warning = $out->msg( 'delete_redirect_and_move_text', $newTalkTitle->getPrefixedText() );
396 $deleteAndMove = [ $newTalkTitle ];
397 }
398 if ( $warning ) {
399 $out->addHTML( Html::warningBox( $warning->parse() ) );
400 } else {
401 $messages = $status->getMessages();
402 if ( $messages ) {
403 if ( $status instanceof PermissionStatus ) {
404 $action_desc = $this->msg( 'action-move' )->plain();
405 $errMsgHtml = $this->msg( 'permissionserrorstext-withaction',
406 count( $messages ), $action_desc )->parseAsBlock();
407 } else {
408 $errMsgHtml = $this->msg( 'cannotmove', count( $messages ) )->parseAsBlock();
409 }
410
411 if ( count( $messages ) == 1 ) {
412 $errMsgHtml .= $this->msg( $messages[0] )->parseAsBlock();
413 } else {
414 $errStr = [];
415
416 foreach ( $messages as $msg ) {
417 $errStr[] = $this->msg( $msg )->parse();
418 }
419
420 // Duplicate errors can easily arise since MovePage checks for both "edit" and "move" against
421 // the target page, and lots of things that block one also block the other
422 $errStr = array_unique( $errStr );
423
424 $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
425 }
426 $out->addHTML( Html::errorBox( $errMsgHtml ) );
427 }
428 $talkMessages = $talkStatus->getMessages();
429 if ( $talkMessages ) {
430 // Can't use permissionerrorstext here since there's no specific action for moving the talk page
431 $errMsgHtml = $this->msg( 'cannotmovetalk', count( $talkMessages ) )->parseAsBlock();
432
433 if ( count( $talkMessages ) == 1 ) {
434 $errMsgHtml .= $this->msg( $talkMessages[0] )->parseAsBlock();
435 } else {
436 $errStr = [];
437
438 foreach ( $talkMessages as $msg ) {
439 $errStr[] = $this->msg( $msg )->parse();
440 }
441
442 $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
443 }
444 $errMsgHtml .= $this->msg( 'movetalk-unselect' )->parse();
445 $out->addHTML( Html::errorBox( $errMsgHtml ) );
446 }
447 }
448
449 if ( $this->restrictionStore->isProtected( $this->oldTitle, 'move' ) ) {
450 # Is the title semi-protected?
451 if ( $this->restrictionStore->isSemiProtected( $this->oldTitle, 'move' ) ) {
452 $noticeMsg = 'semiprotectedpagemovewarning';
453 } else {
454 # Then it must be protected based on static groups (regular)
455 $noticeMsg = 'protectedpagemovewarning';
456 }
457 LogEventsList::showLogExtract(
458 $out,
459 'protect',
460 $this->oldTitle,
461 '',
462 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
463 );
464 }
465 // Intentionally don't check moveTalk here since this is in the form before you specify whether
466 // to move the talk page
467 if ( $talkOK && $oldTalk && $oldTalk->exists() && $this->restrictionStore->isProtected( $oldTalk, 'move' ) ) {
468 # Is the title semi-protected?
469 if ( $this->restrictionStore->isSemiProtected( $oldTalk, 'move' ) ) {
470 $noticeMsg = 'semiprotectedtalkpagemovewarning';
471 } else {
472 # Then it must be protected based on static groups (regular)
473 $noticeMsg = 'protectedtalkpagemovewarning';
474 }
475 LogEventsList::showLogExtract(
476 $out,
477 'protect',
478 $oldTalk,
479 '',
480 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
481 );
482 }
483
484 // Length limit for wpReason and wpNewTitleMain is enforced in the
485 // mediawiki.special.movePage module
486
487 $immovableNamespaces = [];
488 foreach ( $this->getLanguage()->getNamespaces() as $nsId => $_ ) {
489 if ( !$this->nsInfo->isMovable( $nsId ) ) {
490 $immovableNamespaces[] = $nsId;
491 }
492 }
493
494 $moveOverProtection = false;
495 if ( $this->newTitle && $mainOK ) {
496 // If you can't move the page anyway then don't bother displaying warnings
497 // about it being protected
498 if ( $this->restrictionStore->isProtected( $this->newTitle, 'create' ) ) {
499 # Is the title semi-protected?
500 if ( $this->restrictionStore->isSemiProtected( $this->newTitle, 'create' ) ) {
501 $noticeMsg = 'semiprotectedpagemovecreatewarning';
502 } else {
503 # Then it must be protected based on static groups (regular)
504 $noticeMsg = 'protectedpagemovecreatewarning';
505 }
506 LogEventsList::showLogExtract(
507 $out,
508 'protect',
509 $this->newTitle,
510 '',
511 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
512 );
513 $moveOverProtection = true;
514 }
515 $newTalk = $newTitle->getTalkPageIfDefined();
516 if ( $oldTalk && $oldTalk->exists() && $talkOK &&
517 $newTalk && $this->restrictionStore->isProtected( $newTalk, 'create' )
518 ) {
519 # Is the title semi-protected?
520 if ( $this->restrictionStore->isSemiProtected( $newTalk, 'create' ) ) {
521 $noticeMsg = 'semiprotectedpagemovetalkcreatewarning';
522 } else {
523 # Then it must be protected based on static groups (regular)
524 $noticeMsg = 'protectedpagemovetalkcreatewarning';
525 }
526 LogEventsList::showLogExtract(
527 $out,
528 'protect',
529 $newTalk,
530 '',
531 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
532 );
533 $moveOverProtection = true;
534 }
535 }
536
537 $out->enableOOUI();
538 $fields = [];
539
540 $fields[] = new FieldLayout(
541 new ComplexTitleInputWidget( [
542 'id' => 'wpNewTitle',
543 'namespace' => [
544 'id' => 'wpNewTitleNs',
545 'name' => 'wpNewTitleNs',
546 'value' => $newTitle->getNamespace(),
547 'exclude' => $immovableNamespaces,
548 ],
549 'title' => [
550 'id' => 'wpNewTitleMain',
551 'name' => 'wpNewTitleMain',
552 'value' => $newTitle->getText(),
553 // Inappropriate, since we're expecting the user to input a non-existent page's title
554 'suggestions' => false,
555 ],
556 'infusable' => true,
557 ] ),
558 [
559 'label' => $this->msg( 'newtitle' )->text(),
560 'align' => 'top',
561 ]
562 );
563
564 $options = Html::listDropdownOptions(
565 $this->msg( 'movepage-reason-dropdown' )
566 ->page( $this->oldTitle )
567 ->inContentLanguage()
568 ->text(),
569 [ 'other' => $this->msg( 'movereasonotherlist' )->text() ]
570 );
571 $options = Html::listDropdownOptionsOoui( $options );
572
573 $fields[] = new FieldLayout(
574 new DropdownInputWidget( [
575 'name' => 'wpReasonList',
576 'inputId' => 'wpReasonList',
577 'infusable' => true,
578 'value' => $this->getRequest()->getText( 'wpReasonList', 'other' ),
579 'options' => $options,
580 ] ),
581 [
582 'label' => $this->msg( 'movereason' )->text(),
583 'align' => 'top',
584 ]
585 );
586
587 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
588 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
589 // Unicode codepoints.
590 $fields[] = new FieldLayout(
591 new TextInputWidget( [
592 'name' => 'wpReason',
593 'id' => 'wpReason',
594 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
595 'infusable' => true,
596 'value' => $this->getRequest()->getText( 'wpReason' ),
597 ] ),
598 [
599 'label' => $this->msg( 'moveotherreason' )->text(),
600 'align' => 'top',
601 ]
602 );
603
604 if ( $considerTalk ) {
605 $fields[] = new FieldLayout(
606 new CheckboxInputWidget( [
607 'name' => 'wpMovetalk',
608 'id' => 'wpMovetalk',
609 'value' => '1',
610 // It's intentional that this box is still visible and checked by default even if you don't have
611 // permission to move the talk page; wanting to separate a base page from its talk page is so
612 // unusual that you should have to explicitly uncheck the box to do so
613 'selected' => $this->moveTalk,
614 ] ),
615 [
616 'label' => $this->msg( 'movetalk' )->text(),
617 'help' => new HtmlSnippet( $this->msg( 'movepagetalktext' )->parseAsBlock() ),
618 'helpInline' => true,
619 'align' => 'inline',
620 'id' => 'wpMovetalk-field',
621 ]
622 );
623 }
624
625 if ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) {
626 if ( $createRedirect ) {
627 $isChecked = $this->leaveRedirect;
628 $isDisabled = false;
629 } else {
630 $isChecked = false;
631 $isDisabled = true;
632 }
633 $fields[] = new FieldLayout(
634 new CheckboxInputWidget( [
635 'name' => 'wpLeaveRedirect',
636 'id' => 'wpLeaveRedirect',
637 'value' => '1',
638 'selected' => $isChecked,
639 'disabled' => $isDisabled,
640 ] ),
641 [
642 'label' => $this->msg( 'move-leave-redirect' )->text(),
643 'align' => 'inline',
644 ]
645 );
646 }
647
648 if ( $hasRedirects ) {
649 $fields[] = new FieldLayout(
650 new CheckboxInputWidget( [
651 'name' => 'wpFixRedirects',
652 'id' => 'wpFixRedirects',
653 'value' => '1',
654 'selected' => $this->fixRedirects,
655 ] ),
656 [
657 'label' => $this->msg( 'fix-double-redirects' )->text(),
658 'align' => 'inline',
659 ]
660 );
661 }
662
663 if ( $canMoveSubpage ) {
664 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
665 $fields[] = new FieldLayout(
666 new CheckboxInputWidget( [
667 'name' => 'wpMovesubpages',
668 'id' => 'wpMovesubpages',
669 'value' => '1',
670 'selected' => $this->moveSubpages,
671 ] ),
672 [
673 'label' => new HtmlSnippet(
674 $this->msg(
675 ( $this->oldTitle->hasSubpages()
676 ? 'move-subpages'
677 : 'move-talk-subpages' )
678 )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse()
679 ),
680 'align' => 'inline',
681 ]
682 );
683 }
684
685 # Don't allow watching if user is not logged in
686 if ( $user->isRegistered() ) {
687 $watchChecked = ( $this->watch || $this->userOptionsLookup->getBoolOption( $user, 'watchmoves' )
688 || $this->watchlistManager->isWatched( $user, $this->oldTitle ) );
689 $fields[] = new FieldLayout(
690 new CheckboxInputWidget( [
691 'name' => 'wpWatch',
692 'id' => 'watch', # ew
693 'infusable' => true,
694 'value' => '1',
695 'selected' => $watchChecked,
696 ] ),
697 [
698 'label' => $this->msg( 'move-watch' )->text(),
699 'align' => 'inline',
700 ]
701 );
702
703 # Add a dropdown for watchlist expiry times in the form, T261230
704 if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) ) {
705 $expiryOptions = WatchAction::getExpiryOptions(
706 $this->getContext(),
707 $this->watchedItemStore->getWatchedItem( $user, $this->oldTitle )
708 );
709 # Reformat the options to match what DropdownInputWidget wants.
710 $options = [];
711 foreach ( $expiryOptions['options'] as $label => $value ) {
712 $options[] = [ 'data' => $value, 'label' => $label ];
713 }
714
715 $fields[] = new FieldLayout(
716 new DropdownInputWidget( [
717 'name' => 'wpWatchlistExpiry',
718 'id' => 'wpWatchlistExpiry',
719 'infusable' => true,
720 'options' => $options,
721 ] ),
722 [
723 'label' => $this->msg( 'confirm-watch-label' )->text(),
724 'id' => 'wpWatchlistExpiryLabel',
725 'infusable' => true,
726 'align' => 'inline',
727 ]
728 );
729 }
730 }
731
732 $hiddenFields = '';
733 if ( $moveOverShared ) {
734 $hiddenFields .= Html::hidden( 'wpMoveOverSharedFile', '1' );
735 }
736
737 if ( $deleteAndMove ) {
738 // Suppress Phan false positives here - the array is either one or two elements, and is assigned above
739 // so the count clearly distinguishes the two cases
740 if ( count( $deleteAndMove ) == 2 ) {
741 $msg = $this->msg( 'delete_and_move_confirm_2',
742 $deleteAndMove[0]->getPrefixedText(),
743 $deleteAndMove[1]->getPrefixedText()
744 )->text();
745 } else {
746 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
747 $msg = $this->msg( 'delete_and_move_confirm', $deleteAndMove[0]->getPrefixedText() )->text();
748 }
749 $fields[] = new FieldLayout(
750 new CheckboxInputWidget( [
751 'name' => 'wpDeleteAndMove',
752 'id' => 'wpDeleteAndMove',
753 'value' => '1',
754 ] ),
755 [
756 'label' => $msg,
757 'align' => 'inline',
758 ]
759 );
760 }
761 if ( $moveOverProtection ) {
762 $fields[] = new FieldLayout(
763 new CheckboxInputWidget( [
764 'name' => 'wpMoveOverProtection',
765 'id' => 'wpMoveOverProtection',
766 'value' => '1',
767 ] ),
768 [
769 'label' => $this->msg( 'move_over_protection_confirm' )->text(),
770 'align' => 'inline',
771 ]
772 );
773 }
774
775 $fields[] = new FieldLayout(
776 new ButtonInputWidget( [
777 'name' => 'wpMove',
778 'value' => $this->msg( 'movepagebtn' )->text(),
779 'label' => $this->msg( 'movepagebtn' )->text(),
780 'flags' => [ 'primary', 'progressive' ],
781 'type' => 'submit',
782 'accessKey' => Linker::accesskey( 'move' ),
783 'title' => Linker::titleAttrib( 'move' ),
784 ] ),
785 [
786 'align' => 'top',
787 ]
788 );
789
790 $fieldset = new FieldsetLayout( [
791 'label' => $this->msg( 'move-page-legend' )->text(),
792 'id' => 'mw-movepage-table',
793 'items' => $fields,
794 ] );
795
796 $form = new FormLayout( [
797 'method' => 'post',
798 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ),
799 'id' => 'movepage',
800 ] );
801 $form->appendContent(
802 $fieldset,
803 new HtmlSnippet(
804 $hiddenFields .
805 Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
806 Html::hidden( 'wpEditToken', $user->getEditToken() )
807 )
808 );
809
810 $out->addHTML(
811 ( new PanelLayout( [
812 'classes' => [ 'movepage-wrapper' ],
813 'expanded' => false,
814 'padded' => true,
815 'framed' => true,
816 'content' => $form,
817 ] ) )->toString()
818 );
819 if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
820 $link = $this->getLinkRenderer()->makeKnownLink(
821 $this->msg( 'movepage-reason-dropdown' )->inContentLanguage()->getTitle(),
822 $this->msg( 'movepage-edit-reasonlist' )->text(),
823 [],
824 [ 'action' => 'edit' ]
825 );
826 $out->addHTML( Html::rawElement( 'p', [ 'class' => 'mw-movepage-editreasons' ], $link ) );
827 }
828
829 $this->showLogFragment( $this->oldTitle );
830 $this->showSubpages( $this->oldTitle );
831 }
832
833 private function vacateTitle( Title $title, User $user, Title $oldTitle ): StatusValue {
834 if ( !$title->exists() ) {
835 return StatusValue::newGood();
836 }
837 $redir2 = $title->isSingleRevRedirect();
838
839 $permStatus = $this->permManager->getPermissionStatus(
840 $redir2 ? 'delete-redirect' : 'delete',
841 $user, $title
842 );
843 if ( !$permStatus->isGood() ) {
844 if ( $redir2 ) {
845 if ( !$this->permManager->userCan( 'delete', $user, $title ) ) {
846 // Cannot delete-redirect, or delete normally
847 return $permStatus;
848 } else {
849 // Cannot delete-redirect, but can delete normally,
850 // so log as a normal deletion
851 $redir2 = false;
852 }
853 } else {
854 // Cannot delete normally
855 return $permStatus;
856 }
857 }
858
859 $page = $this->wikiPageFactory->newFromTitle( $title );
860 $delPage = $this->deletePageFactory->newDeletePage( $page, $user );
861
862 // Small safety margin to guard against concurrent edits
863 if ( $delPage->isBatchedDelete( 5 ) ) {
864 return StatusValue::newFatal( 'movepage-delete-first' );
865 }
866
867 $reason = $this->msg( 'delete_and_move_reason', $oldTitle->getPrefixedText() )->inContentLanguage()->text();
868
869 // Delete an associated image if there is
870 if ( $title->getNamespace() === NS_FILE ) {
871 $file = $this->repoGroup->getLocalRepo()->newFile( $title );
872 $file->load( IDBAccessObject::READ_LATEST );
873 if ( $file->exists() ) {
874 $file->deleteFile( $reason, $user, false );
875 }
876 }
877
878 $deletionLog = $redir2 ? 'delete_redir2' : 'delete';
879 $deleteStatus = $delPage
880 ->setLogSubtype( $deletionLog )
881 // Should be redundant thanks to the isBatchedDelete check above.
882 ->forceImmediate( true )
883 ->deleteUnsafe( $reason );
884
885 return $deleteStatus;
886 }
887
888 private function doSubmit() {
889 $user = $this->getUser();
890
891 if ( $user->pingLimiter( 'move' ) ) {
892 throw new ThrottledError;
893 }
894
895 $ot = $this->oldTitle;
896 $nt = $this->newTitle;
897
898 # don't allow moving to pages with # in
899 if ( !$nt || $nt->hasFragment() ) {
900 $this->showForm( StatusValue::newFatal( 'badtitletext' ) );
901
902 return;
903 }
904
905 $oldTalk = $ot->getTalkPageIfDefined();
906 $newTalk = $nt->getTalkPageIfDefined();
907
908 if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
909 $this->moveTalk = false;
910 }
911
912 # Show a warning if the target file exists on a shared repo
913 if ( $nt->getNamespace() === NS_FILE
914 && !( $this->moveOverShared && $this->permManager->userHasRight( $user, 'reupload-shared' ) )
915 && !$this->repoGroup->getLocalRepo()->findFile( $nt )
916 && $this->repoGroup->findFile( $nt )
917 ) {
918 $this->showForm( StatusValue::newFatal( 'file-exists-sharedrepo' ) );
919
920 return;
921 }
922
923 # Show a warning if procted (showForm handles the warning )
924 if ( !$this->moveOverProtection ) {
925 if ( $this->restrictionStore->isProtected( $nt, 'create' ) ) {
926 $this->showForm();
927 return;
928 }
929 if ( $this->moveTalk && $newTalk && $this->restrictionStore->isProtected( $newTalk, 'create' ) ) {
930 $this->showForm();
931 return;
932 }
933 }
934
935 $handler = $this->contentHandlerFactory->getContentHandler( $ot->getContentModel() );
936
937 if ( !$handler->supportsRedirects() || (
938 // Do not create redirects for wikitext message overrides (T376399).
939 // Maybe one day they will have a custom content model and this special case won't be needed.
940 $ot->getNamespace() === NS_MEDIAWIKI &&
941 $ot->getContentModel() === CONTENT_MODEL_WIKITEXT
942 ) ) {
943 $createRedirect = false;
944 } elseif ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) {
945 $createRedirect = $this->leaveRedirect;
946 } else {
947 $createRedirect = true;
948 }
949
950 // Check perms
951 $mp = $this->movePageFactory->newMovePage( $ot, $nt );
952 $permStatusMain = $mp->authorizeMove( $this->getAuthority(), $this->reason );
953 $permStatusMain->merge( $mp->isValidMove() );
954
955 $onlyMovingTalkSubpages = false;
956 if ( $this->moveTalk && $oldTalk && $newTalk ) {
957 $mpTalk = $this->movePageFactory->newMovePage( $oldTalk, $newTalk );
958 $permStatusTalk = $mpTalk->authorizeMove( $this->getAuthority(), $this->reason );
959 $permStatusTalk->merge( $mpTalk->isValidMove() );
960 // Per the definition of $considerTalk in showForm you might be trying to move
961 // subpages of the talk even if the talk itself doesn't exist, so let that happen
962 if ( !$permStatusTalk->isOK() &&
963 !$permStatusTalk->hasMessagesExcept( 'movepage-source-doesnt-exist' )
964 ) {
965 $permStatusTalk->setOK( true );
966 $onlyMovingTalkSubpages = true;
967 }
968 } else {
969 $permStatusTalk = StatusValue::newGood();
970 $mpTalk = null;
971 }
972
973 if ( $this->deleteAndMove ) {
974 // This is done before the deletion (in order to minimize the impact of T265792)
975 // so ignore "it already exists" checks (they will be repeated after the deletion)
976 if ( $permStatusMain->hasMessagesExcept( 'redirectexists', 'articleexists' ) ||
977 ( !$onlyMovingTalkSubpages &&
978 $permStatusTalk->hasMessagesExcept( 'redirectexists', 'articleexists' ) )
979 ) {
980 $this->showForm( $permStatusMain, $permStatusTalk );
981 return;
982 }
983 // If the code gets here, then it's passed all permission checks and the move should succeed
984 // so start deleting things.
985 // FIXME: This isn't atomic; it could delete things even if the move will later fail (T265792)
986 // For example, if you manually specify deleteAndMove in the URL (the form UI won't show the checkbox)
987 // and have `delete-redirect` and the main page is a single-revision redirect
988 // but the talk page isn't it will delete the redirect and then fail, leaving it deleted
989 $deleteStatus = $this->vacateTitle( $nt, $user, $ot );
990 if ( !$deleteStatus->isGood() ) {
991 $this->showForm( $deleteStatus );
992 return;
993 }
994 if ( $this->moveTalk && $oldTalk && $newTalk ) {
995 $deleteStatus = $this->vacateTitle( $newTalk, $user, $oldTalk );
996 if ( !$deleteStatus->isGood() ) {
997 // Ideally we would specify that the subject page redirect was deleted
998 // but see the FIXME above
999 $this->showForm( StatusValue::newGood(), $deleteStatus );
1000 return;
1001 }
1002 }
1003 } elseif ( !$permStatusMain->isOK() || !$permStatusTalk->isOK() ) {
1004 // If we're not going to delete then bail on all errors
1005 $this->showForm( $permStatusMain, $permStatusTalk );
1006 return;
1007 }
1008
1009 // Now we've confirmed you can do all of the moves you want and proceeding won't leave things inconsistent
1010 // so actually move the main page
1011 $mainStatus = $mp->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect );
1012 if ( !$mainStatus->isOK() ) {
1013 $this->showForm( $mainStatus );
1014 return;
1015 }
1016
1017 $fixRedirects = $this->fixRedirects && $this->getConfig()->get( MainConfigNames::FixDoubleRedirects );
1018 if ( $fixRedirects ) {
1019 DoubleRedirectJob::fixRedirects( 'move', $ot );
1020 }
1021
1022 // Now try to move the talk page
1023 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
1024
1025 $moveStatuses = [];
1026 $talkStatus = null;
1027 if ( $mpTalk && !$onlyMovingTalkSubpages ) {
1028 $talkStatus = $mpTalk->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect );
1029 // moveIfAllowed returns a Status with an array as a value, however moveSubpages per-title statuses
1030 // have strings as values. Massage this status into the moveSubpages format so it fits in with
1031 // the later calls
1032 '@phan-var Status<string> $talkStatus';
1033 $talkStatus->value = $newTalk->getPrefixedText();
1034 $moveStatuses[$oldTalk->getPrefixedText()] = $talkStatus;
1035 }
1036
1037 // Now try to move subpages if asked
1038 if ( $this->moveSubpages ) {
1039 if ( $this->permManager->userCan( 'move-subpages', $user, $ot ) ) {
1040 $mp->setMaximumMovedPages( $maximumMovedPages - count( $moveStatuses ) );
1041 $subpageStatus = $mp->moveSubpagesIfAllowed( $this->getAuthority(), $this->reason, $createRedirect );
1042 $moveStatuses = array_merge( $moveStatuses, $subpageStatus->value );
1043 }
1044 if ( $mpTalk && $oldTalk && $maximumMovedPages > count( $moveStatuses ) &&
1045 $this->permManager->userCan( 'move-subpages', $user, $oldTalk ) &&
1046 ( $onlyMovingTalkSubpages || $talkStatus->isOK() )
1047 ) {
1048 $mpTalk->setMaximumMovedPages( $maximumMovedPages - count( $moveStatuses ) );
1049 $talkSubStatus = $mpTalk->moveSubpagesIfAllowed(
1050 $this->getAuthority(), $this->reason, $createRedirect
1051 );
1052 $moveStatuses = array_merge( $moveStatuses, $talkSubStatus->value );
1053 }
1054 }
1055
1056 // Now we've moved everything we're going to move, so post-process the output,
1057 // create the UI, and fix double redirects
1058 $out = $this->getOutput();
1059 $out->setPageTitleMsg( $this->msg( 'pagemovedsub' ) );
1060
1061 $linkRenderer = $this->getLinkRenderer();
1062 $oldLink = $linkRenderer->makeLink(
1063 $ot,
1064 null,
1065 [ 'id' => 'movepage-oldlink' ],
1066 [ 'redirect' => 'no' ]
1067 );
1068 $newLink = $linkRenderer->makeKnownLink(
1069 $nt,
1070 null,
1071 [ 'id' => 'movepage-newlink' ]
1072 );
1073 $oldText = $ot->getPrefixedText();
1074 $newText = $nt->getPrefixedText();
1075
1076 $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink,
1077 $newLink )->params( $oldText, $newText )->parseAsBlock() );
1078 $out->addWikiMsg( isset( $mainStatus->getValue()['redirectRevision'] ) ?
1079 'movepage-moved-redirect' :
1080 'movepage-moved-noredirect' );
1081
1082 $this->getHookRunner()->onSpecialMovepageAfterMove( $this, $ot, $nt );
1083
1084 $extraOutput = [];
1085 foreach ( $moveStatuses as $oldSubpage => $subpageStatus ) {
1086 if ( $subpageStatus->hasMessage( 'movepage-max-pages' ) ) {
1087 $extraOutput[] = $this->msg( 'movepage-max-pages' )
1088 ->numParams( $maximumMovedPages )->escaped();
1089 continue;
1090 }
1091 $oldSubpage = Title::newFromText( $oldSubpage );
1092 $newSubpage = Title::newFromText( $subpageStatus->value );
1093 if ( $subpageStatus->isGood() ) {
1094 if ( $fixRedirects ) {
1095 DoubleRedirectJob::fixRedirects( 'move', $oldSubpage );
1096 }
1097 $oldLink = $linkRenderer->makeLink( $oldSubpage, null, [], [ 'redirect' => "no" ] );
1098 $newLink = $linkRenderer->makeKnownLink( $newSubpage );
1099
1100 $extraOutput[] = $this->msg( 'movepage-page-moved' )->rawParams(
1101 $oldLink, $newLink
1102 )->escaped();
1103 } elseif ( $subpageStatus->hasMessage( 'articleexists' )
1104 || $subpageStatus->hasMessage( 'redirectexists' )
1105 ) {
1106 $link = $linkRenderer->makeKnownLink( $newSubpage );
1107 $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
1108 } else {
1109 $oldLink = $linkRenderer->makeKnownLink( $oldSubpage );
1110 if ( $newSubpage ) {
1111 $newLink = $linkRenderer->makeLink( $newSubpage );
1112 } else {
1113 // It's not a valid title
1114 $newLink = htmlspecialchars( $subpageStatus->value );
1115 }
1116 $extraOutput[] = $this->msg( 'movepage-page-unmoved' )
1117 ->rawParams( $oldLink, $newLink )->escaped();
1118 }
1119 }
1120
1121 if ( $extraOutput !== [] ) {
1122 $out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
1123 }
1124
1125 # Deal with watches (we don't watch subpages)
1126 # Get text from expiry selection dropdown, T261230
1127 $expiry = $this->getRequest()->getText( 'wpWatchlistExpiry' );
1128 if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && $expiry !== '' ) {
1129 $expiry = ExpiryDef::normalizeExpiry( $expiry, TS::ISO_8601 );
1130 } else {
1131 $expiry = null;
1132 }
1133
1134 $this->watchlistManager->setWatch(
1135 $this->watch,
1136 $this->getAuthority(),
1137 $ot,
1138 $expiry
1139 );
1140
1141 $this->watchlistManager->setWatch(
1142 $this->watch,
1143 $this->getAuthority(),
1144 $nt,
1145 $expiry
1146 );
1147 }
1148
1149 private function showLogFragment( Title $title ) {
1150 $moveLogPage = new LogPage( 'move' );
1151 $out = $this->getOutput();
1152 $out->addHTML( Html::element( 'h2', [], $moveLogPage->getName()->text() ) );
1153 LogEventsList::showLogExtract( $out, 'move', $title );
1154 }
1155
1162 private function showSubpages( $title ) {
1163 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
1164 $nsHasSubpages = $this->nsInfo->hasSubpages( $title->getNamespace() );
1165 $subpages = $title->getSubpages( $maximumMovedPages + 1 );
1166 $count = $subpages instanceof TitleArrayFromResult ? $subpages->count() : 0;
1167
1168 $titleIsTalk = $title->isTalkPage();
1169 $subpagesTalk = $title->getTalkPage()->getSubpages( $maximumMovedPages + 1 );
1170 $countTalk = $subpagesTalk instanceof TitleArrayFromResult ? $subpagesTalk->count() : 0;
1171 $totalCount = $count + $countTalk;
1172
1173 if ( !$nsHasSubpages && $countTalk == 0 ) {
1174 return;
1175 }
1176
1177 $this->getOutput()->wrapWikiMsg(
1178 '== $1 ==',
1179 [ 'movesubpage', ( $titleIsTalk ? $count : $totalCount ) ]
1180 );
1181
1182 if ( $nsHasSubpages ) {
1183 $this->showSubpagesList(
1184 $subpages, $count, 'movesubpagetext', 'movesubpagetext-truncated', true
1185 );
1186 }
1187
1188 if ( !$titleIsTalk && $countTalk > 0 ) {
1189 $this->showSubpagesList(
1190 $subpagesTalk, $countTalk, 'movesubpagetalktext', 'movesubpagetalktext-truncated'
1191 );
1192 }
1193 }
1194
1195 private function showSubpagesList(
1196 TitleArrayFromResult $subpages, int $pagecount, string $msg, string $truncatedMsg, bool $noSubpageMsg = false
1197 ) {
1198 $out = $this->getOutput();
1199
1200 # No subpages.
1201 if ( $pagecount == 0 && $noSubpageMsg ) {
1202 $out->addWikiMsg( 'movenosubpage' );
1203 return;
1204 }
1205
1206 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
1207
1208 if ( $pagecount > $maximumMovedPages ) {
1209 $subpages = $this->truncateSubpagesList( $subpages );
1210 $out->addWikiMsg( $truncatedMsg, $this->getLanguage()->formatNum( $maximumMovedPages ) );
1211 } else {
1212 $out->addWikiMsg( $msg, $this->getLanguage()->formatNum( $pagecount ) );
1213 }
1214 $out->addHTML( "<ul>\n" );
1215
1216 $this->linkBatchFactory->newLinkBatch( $subpages )
1217 ->setCaller( __METHOD__ )
1218 ->execute();
1219 $linkRenderer = $this->getLinkRenderer();
1220
1221 foreach ( $subpages as $subpage ) {
1222 $link = $linkRenderer->makeLink( $subpage );
1223 $out->addHTML( "<li>$link</li>\n" );
1224 }
1225 $out->addHTML( "</ul>\n" );
1226 }
1227
1228 private function truncateSubpagesList( iterable $subpages ): array {
1229 $returnArray = [];
1230 foreach ( $subpages as $subpage ) {
1231 $returnArray[] = $subpage;
1232 if ( count( $returnArray ) >= $this->getConfig()->get( MainConfigNames::MaximumMovedPages ) ) {
1233 break;
1234 }
1235 }
1236 return $returnArray;
1237 }
1238
1247 public function prefixSearchSubpages( $search, $limit, $offset ) {
1248 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
1249 }
1250
1252 protected function getGroupName() {
1253 return 'pagetools';
1254 }
1255}
1256
1261class_alias( SpecialMovePage::class, 'MovePageForm' );
const NS_USER
Definition Defines.php:53
const NS_FILE
Definition Defines.php:57
const NS_MEDIAWIKI
Definition Defines.php:59
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:235
const NS_CATEGORY
Definition Defines.php:65
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Page addition to a user's watchlist.
Handle database storage of comments such as edit summaries and log reasons.
An error page which can definitely be safely rendered using the OutputPage.
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the user hits a rate limit.
Prioritized list of file repositories.
Definition RepoGroup.php:30
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Fix any double redirects after moving a page.
static fixRedirects( $reason, $redirTitle)
Insert jobs into the job queue to fix redirects to the given title.
Some internal bits split of from Skin.php.
Definition Linker.php:47
Class to simplify the use of log pages.
Definition LogPage.php:35
A class containing constants representing the names of configuration variables.
const FixDoubleRedirects
Name constant for the FixDoubleRedirects setting, for use with Config::get()
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
const MaximumMovedPages
Name constant for the MaximumMovedPages setting, for use with Config::get()
Factory for LinkBatch objects to batch query page metadata.
Service for creating WikiPage objects.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
A StatusValue for permission errors.
Factory class for SearchEngine.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Shortcut to construct a special page which is unlisted by default.
Implement Special:Movepage for changing page titles.
__construct(private readonly MovePageFactory $movePageFactory, private readonly PermissionManager $permManager, private readonly UserOptionsLookup $userOptionsLookup, private readonly IConnectionProvider $dbProvider, private readonly IContentHandlerFactory $contentHandlerFactory, private readonly NamespaceInfo $nsInfo, private readonly LinkBatchFactory $linkBatchFactory, private readonly RepoGroup $repoGroup, private readonly WikiPageFactory $wikiPageFactory, private readonly SearchEngineFactory $searchEngineFactory, private readonly WatchlistManager $watchlistManager, private readonly WatchedItemStore $watchedItemStore, private readonly RestrictionStore $restrictionStore, private readonly TitleFactory $titleFactory, private readonly DeletePageFactory $deletePageFactory,)
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
execute( $par)
Default execute method Checks user permissions.This must be overridden by subclasses; it will be made...
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki....
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:69
getTalkPageIfDefined()
Get a Title object associated with the talk page of this article, if such a talk page can exist.
Definition Title.php:1645
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1010
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1857
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:130
Storage layer class for WatchedItems.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
Service for page delete actions.
Service for page rename actions.
Provide primary and replica IDatabase connections.
Interface for database access objects.
element(SerializerNode $parent, SerializerNode $node, $contents)
msg( $key,... $params)