Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
50.52% |
337 / 667 |
|
27.27% |
3 / 11 |
CRAP | |
0.00% |
0 / 1 |
| SpecialMovePage | |
50.60% |
337 / 666 |
|
27.27% |
3 / 11 |
2365.68 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
| doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
76.00% |
38 / 50 |
|
0.00% |
0 / 1 |
13.99 | |||
| showForm | |
67.42% |
238 / 353 |
|
0.00% |
0 / 1 |
123.38 | |||
| doSubmit | |
0.00% |
0 / 194 |
|
0.00% |
0 / 1 |
3080 | |||
| showLogFragment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| showSubpages | |
86.36% |
19 / 22 |
|
0.00% |
0 / 1 |
9.21 | |||
| showSubpagesList | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
5.03 | |||
| truncateSubpagesList | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| prefixSearchSubpages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Specials; |
| 8 | |
| 9 | use MediaWiki\Actions\WatchAction; |
| 10 | use MediaWiki\Cache\LinkBatchFactory; |
| 11 | use MediaWiki\CommentStore\CommentStore; |
| 12 | use MediaWiki\Content\IContentHandlerFactory; |
| 13 | use MediaWiki\Exception\ErrorPageError; |
| 14 | use MediaWiki\Exception\PermissionsError; |
| 15 | use MediaWiki\Exception\ThrottledError; |
| 16 | use MediaWiki\FileRepo\RepoGroup; |
| 17 | use MediaWiki\Html\Html; |
| 18 | use MediaWiki\JobQueue\Jobs\DoubleRedirectJob; |
| 19 | use MediaWiki\Logging\LogEventsList; |
| 20 | use MediaWiki\Logging\LogPage; |
| 21 | use MediaWiki\MainConfigNames; |
| 22 | use MediaWiki\Page\DeletePageFactory; |
| 23 | use MediaWiki\Page\MovePageFactory; |
| 24 | use MediaWiki\Page\WikiPageFactory; |
| 25 | use MediaWiki\Permissions\PermissionManager; |
| 26 | use MediaWiki\Permissions\PermissionStatus; |
| 27 | use MediaWiki\Permissions\RestrictionStore; |
| 28 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
| 29 | use MediaWiki\Title\NamespaceInfo; |
| 30 | use MediaWiki\Title\Title; |
| 31 | use MediaWiki\Title\TitleArrayFromResult; |
| 32 | use MediaWiki\Title\TitleFactory; |
| 33 | use MediaWiki\User\Options\UserOptionsLookup; |
| 34 | use MediaWiki\Watchlist\WatchedItemStore; |
| 35 | use MediaWiki\Watchlist\WatchlistManager; |
| 36 | use MediaWiki\Widget\ComplexTitleInputWidget; |
| 37 | use OOUI\ButtonInputWidget; |
| 38 | use OOUI\CheckboxInputWidget; |
| 39 | use OOUI\DropdownInputWidget; |
| 40 | use OOUI\FieldLayout; |
| 41 | use OOUI\FieldsetLayout; |
| 42 | use OOUI\FormLayout; |
| 43 | use OOUI\HtmlSnippet; |
| 44 | use OOUI\PanelLayout; |
| 45 | use OOUI\TextInputWidget; |
| 46 | use SearchEngineFactory; |
| 47 | use StatusValue; |
| 48 | use Wikimedia\ParamValidator\TypeDef\ExpiryDef; |
| 49 | use Wikimedia\Rdbms\IConnectionProvider; |
| 50 | use Wikimedia\Rdbms\IDBAccessObject; |
| 51 | use Wikimedia\Rdbms\IExpression; |
| 52 | use Wikimedia\Rdbms\LikeValue; |
| 53 | use Wikimedia\StringUtils\StringUtils; |
| 54 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 55 | |
| 56 | /** |
| 57 | * Implement Special:Movepage for changing page titles |
| 58 | * |
| 59 | * @ingroup SpecialPage |
| 60 | */ |
| 61 | class SpecialMovePage extends UnlistedSpecialPage { |
| 62 | /** @var Title */ |
| 63 | protected $oldTitle = null; |
| 64 | |
| 65 | /** @var Title */ |
| 66 | protected $newTitle; |
| 67 | |
| 68 | /** @var string Text input */ |
| 69 | protected $reason; |
| 70 | |
| 71 | /** @var bool */ |
| 72 | protected $moveTalk; |
| 73 | |
| 74 | /** @var bool */ |
| 75 | protected $deleteAndMove; |
| 76 | |
| 77 | /** @var bool */ |
| 78 | protected $moveSubpages; |
| 79 | |
| 80 | /** @var bool */ |
| 81 | protected $fixRedirects; |
| 82 | |
| 83 | /** @var bool */ |
| 84 | protected $leaveRedirect; |
| 85 | |
| 86 | /** @var bool */ |
| 87 | protected $moveOverShared; |
| 88 | |
| 89 | /** @var bool */ |
| 90 | private $watch = false; |
| 91 | |
| 92 | private MovePageFactory $movePageFactory; |
| 93 | private PermissionManager $permManager; |
| 94 | private UserOptionsLookup $userOptionsLookup; |
| 95 | private IConnectionProvider $dbProvider; |
| 96 | private IContentHandlerFactory $contentHandlerFactory; |
| 97 | private NamespaceInfo $nsInfo; |
| 98 | private LinkBatchFactory $linkBatchFactory; |
| 99 | private RepoGroup $repoGroup; |
| 100 | private WikiPageFactory $wikiPageFactory; |
| 101 | private SearchEngineFactory $searchEngineFactory; |
| 102 | private WatchlistManager $watchlistManager; |
| 103 | private WatchedItemStore $watchedItemStore; |
| 104 | private RestrictionStore $restrictionStore; |
| 105 | private TitleFactory $titleFactory; |
| 106 | private DeletePageFactory $deletePageFactory; |
| 107 | |
| 108 | public function __construct( |
| 109 | MovePageFactory $movePageFactory, |
| 110 | PermissionManager $permManager, |
| 111 | UserOptionsLookup $userOptionsLookup, |
| 112 | IConnectionProvider $dbProvider, |
| 113 | IContentHandlerFactory $contentHandlerFactory, |
| 114 | NamespaceInfo $nsInfo, |
| 115 | LinkBatchFactory $linkBatchFactory, |
| 116 | RepoGroup $repoGroup, |
| 117 | WikiPageFactory $wikiPageFactory, |
| 118 | SearchEngineFactory $searchEngineFactory, |
| 119 | WatchlistManager $watchlistManager, |
| 120 | WatchedItemStore $watchedItemStore, |
| 121 | RestrictionStore $restrictionStore, |
| 122 | TitleFactory $titleFactory, |
| 123 | DeletePageFactory $deletePageFactory |
| 124 | ) { |
| 125 | parent::__construct( 'Movepage' ); |
| 126 | $this->movePageFactory = $movePageFactory; |
| 127 | $this->permManager = $permManager; |
| 128 | $this->userOptionsLookup = $userOptionsLookup; |
| 129 | $this->dbProvider = $dbProvider; |
| 130 | $this->contentHandlerFactory = $contentHandlerFactory; |
| 131 | $this->nsInfo = $nsInfo; |
| 132 | $this->linkBatchFactory = $linkBatchFactory; |
| 133 | $this->repoGroup = $repoGroup; |
| 134 | $this->wikiPageFactory = $wikiPageFactory; |
| 135 | $this->searchEngineFactory = $searchEngineFactory; |
| 136 | $this->watchlistManager = $watchlistManager; |
| 137 | $this->watchedItemStore = $watchedItemStore; |
| 138 | $this->restrictionStore = $restrictionStore; |
| 139 | $this->titleFactory = $titleFactory; |
| 140 | $this->deletePageFactory = $deletePageFactory; |
| 141 | } |
| 142 | |
| 143 | /** @inheritDoc */ |
| 144 | public function doesWrites() { |
| 145 | return true; |
| 146 | } |
| 147 | |
| 148 | /** @inheritDoc */ |
| 149 | public function execute( $par ) { |
| 150 | $this->useTransactionalTimeLimit(); |
| 151 | $this->checkReadOnly(); |
| 152 | $this->setHeaders(); |
| 153 | $this->outputHeader(); |
| 154 | |
| 155 | $request = $this->getRequest(); |
| 156 | |
| 157 | // Beware: The use of WebRequest::getText() is wanted! See T22365 |
| 158 | $target = $par ?? $request->getText( 'target' ); |
| 159 | $oldTitleText = $request->getText( 'wpOldTitle', $target ); |
| 160 | $this->oldTitle = Title::newFromText( $oldTitleText ); |
| 161 | |
| 162 | if ( !$this->oldTitle ) { |
| 163 | // Either oldTitle wasn't passed, or newFromText returned null |
| 164 | throw new ErrorPageError( 'notargettitle', 'notargettext' ); |
| 165 | } |
| 166 | $this->getOutput()->addBacklinkSubtitle( $this->oldTitle ); |
| 167 | // Various uses of Html::errorBox and Html::warningBox. |
| 168 | $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
| 169 | |
| 170 | if ( !$this->oldTitle->exists() ) { |
| 171 | throw new ErrorPageError( 'nopagetitle', 'nopagetext' ); |
| 172 | } |
| 173 | |
| 174 | $newTitleTextMain = $request->getText( 'wpNewTitleMain' ); |
| 175 | $newTitleTextNs = $request->getInt( 'wpNewTitleNs', $this->oldTitle->getNamespace() ); |
| 176 | // Backwards compatibility for forms submitting here from other sources |
| 177 | // which is more common than it should be. |
| 178 | $newTitleText_bc = $request->getText( 'wpNewTitle' ); |
| 179 | $this->newTitle = strlen( $newTitleText_bc ) > 0 |
| 180 | ? Title::newFromText( $newTitleText_bc ) |
| 181 | : Title::makeTitleSafe( $newTitleTextNs, $newTitleTextMain ); |
| 182 | |
| 183 | $user = $this->getUser(); |
| 184 | $isSubmit = $request->getRawVal( 'action' ) === 'submit' && $request->wasPosted(); |
| 185 | |
| 186 | $reasonList = $request->getText( 'wpReasonList', 'other' ); |
| 187 | $reason = $request->getText( 'wpReason' ); |
| 188 | if ( $reasonList === 'other' ) { |
| 189 | $this->reason = $reason; |
| 190 | } elseif ( $reason !== '' ) { |
| 191 | $this->reason = $reasonList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reason; |
| 192 | } else { |
| 193 | $this->reason = $reasonList; |
| 194 | } |
| 195 | // Default to checked, but don't fill in true during submission (browsers only submit checked values) |
| 196 | // TODO: Use HTMLForm to take care of this. |
| 197 | $def = !$isSubmit; |
| 198 | $this->moveTalk = $request->getBool( 'wpMovetalk', $def ); |
| 199 | $this->fixRedirects = $request->getBool( 'wpFixRedirects', $def ); |
| 200 | $this->leaveRedirect = $request->getBool( 'wpLeaveRedirect', $def ); |
| 201 | // T222953: Tick the "move subpages" box by default |
| 202 | $this->moveSubpages = $request->getBool( 'wpMovesubpages', $def ); |
| 203 | $this->deleteAndMove = $request->getBool( 'wpDeleteAndMove' ); |
| 204 | $this->moveOverShared = $request->getBool( 'wpMoveOverSharedFile' ); |
| 205 | $this->watch = $request->getCheck( 'wpWatch' ) && $user->isRegistered(); |
| 206 | |
| 207 | // Similar to other SpecialPage/Action classes, when tokens fail (likely due to reset or expiry), |
| 208 | // do not show an error but show the form again for easy re-submit. |
| 209 | if ( $isSubmit && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { |
| 210 | // Check rights |
| 211 | $permStatus = $this->permManager->getPermissionStatus( 'move', $user, $this->oldTitle, |
| 212 | PermissionManager::RIGOR_SECURE ); |
| 213 | // If the account is "hard" blocked, auto-block IP |
| 214 | $user->scheduleSpreadBlock(); |
| 215 | if ( !$permStatus->isGood() ) { |
| 216 | throw new PermissionsError( 'move', $permStatus ); |
| 217 | } |
| 218 | $this->doSubmit(); |
| 219 | } else { |
| 220 | // Avoid primary DB connection on form view (T283265) |
| 221 | $permStatus = $this->permManager->getPermissionStatus( 'move', $user, $this->oldTitle, |
| 222 | PermissionManager::RIGOR_FULL ); |
| 223 | if ( !$permStatus->isGood() ) { |
| 224 | $user->scheduleSpreadBlock(); |
| 225 | throw new PermissionsError( 'move', $permStatus ); |
| 226 | } |
| 227 | $this->showForm(); |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * Show the form |
| 233 | * |
| 234 | * @param ?StatusValue $status Form submission status. |
| 235 | * If it is a PermissionStatus, a special message will be shown. |
| 236 | */ |
| 237 | private function showForm( ?StatusValue $status = null ) { |
| 238 | $this->getSkin()->setRelevantTitle( $this->oldTitle ); |
| 239 | |
| 240 | $out = $this->getOutput(); |
| 241 | $out->setPageTitleMsg( $this->msg( 'move-page' )->plaintextParams( $this->oldTitle->getPrefixedText() ) ); |
| 242 | $out->addModuleStyles( [ |
| 243 | 'mediawiki.special', |
| 244 | 'mediawiki.interface.helpers.styles' |
| 245 | ] ); |
| 246 | $out->addModules( 'mediawiki.misc-authed-ooui' ); |
| 247 | $this->addHelpLink( 'Help:Moving a page' ); |
| 248 | |
| 249 | $handler = $this->contentHandlerFactory |
| 250 | ->getContentHandler( $this->oldTitle->getContentModel() ); |
| 251 | $createRedirect = $handler->supportsRedirects() && !( |
| 252 | // Do not create redirects for wikitext message overrides (T376399). |
| 253 | // Maybe one day they will have a custom content model and this special case won't be needed. |
| 254 | $this->oldTitle->getNamespace() === NS_MEDIAWIKI && |
| 255 | $this->oldTitle->getContentModel() === CONTENT_MODEL_WIKITEXT |
| 256 | ); |
| 257 | |
| 258 | if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) { |
| 259 | $out->addWikiMsg( 'movepagetext' ); |
| 260 | } else { |
| 261 | $out->addWikiMsg( $createRedirect ? |
| 262 | 'movepagetext-noredirectfixer' : |
| 263 | 'movepagetext-noredirectsupport' ); |
| 264 | } |
| 265 | |
| 266 | if ( $this->oldTitle->getNamespace() === NS_USER && !$this->oldTitle->isSubpage() ) { |
| 267 | $out->addHTML( |
| 268 | Html::warningBox( |
| 269 | $out->msg( 'moveuserpage-warning' )->parse(), |
| 270 | 'mw-moveuserpage-warning' |
| 271 | ) |
| 272 | ); |
| 273 | } elseif ( $this->oldTitle->getNamespace() === NS_CATEGORY ) { |
| 274 | $out->addHTML( |
| 275 | Html::warningBox( |
| 276 | $out->msg( 'movecategorypage-warning' )->parse(), |
| 277 | 'mw-movecategorypage-warning' |
| 278 | ) |
| 279 | ); |
| 280 | } |
| 281 | |
| 282 | $deleteAndMove = false; |
| 283 | $moveOverShared = false; |
| 284 | |
| 285 | $user = $this->getUser(); |
| 286 | $newTitle = $this->newTitle; |
| 287 | |
| 288 | if ( !$newTitle ) { |
| 289 | # Show the current title as a default |
| 290 | # when the form is first opened. |
| 291 | $newTitle = $this->oldTitle; |
| 292 | } elseif ( !$status ) { |
| 293 | # If a title was supplied, probably from the move log revert |
| 294 | # link, check for validity. We can then show some diagnostic |
| 295 | # information and save a click. |
| 296 | $mp = $this->movePageFactory->newMovePage( $this->oldTitle, $newTitle ); |
| 297 | $status = $mp->isValidMove(); |
| 298 | $status->merge( $mp->probablyCanMove( $this->getAuthority() ) ); |
| 299 | } |
| 300 | if ( !$status ) { |
| 301 | $status = StatusValue::newGood(); |
| 302 | } |
| 303 | |
| 304 | if ( count( $status->getMessages() ) == 1 ) { |
| 305 | if ( $status->hasMessage( 'articleexists' ) |
| 306 | && $this->permManager->quickUserCan( 'delete', $user, $newTitle ) |
| 307 | ) { |
| 308 | $out->addHTML( |
| 309 | Html::warningBox( |
| 310 | $out->msg( 'delete_and_move_text', $newTitle->getPrefixedText() )->parse() |
| 311 | ) |
| 312 | ); |
| 313 | $deleteAndMove = true; |
| 314 | $status = StatusValue::newGood(); |
| 315 | } elseif ( $status->hasMessage( 'redirectexists' ) && ( |
| 316 | // Any user that can delete normally can also delete a redirect here |
| 317 | $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) || |
| 318 | $this->permManager->quickUserCan( 'delete', $user, $newTitle ) ) |
| 319 | ) { |
| 320 | $out->addHTML( |
| 321 | Html::warningBox( |
| 322 | $out->msg( 'delete_redirect_and_move_text', $newTitle->getPrefixedText() )->parse() |
| 323 | ) |
| 324 | ); |
| 325 | $deleteAndMove = true; |
| 326 | $status = StatusValue::newGood(); |
| 327 | } elseif ( $status->hasMessage( 'file-exists-sharedrepo' ) |
| 328 | && $this->permManager->userHasRight( $user, 'reupload-shared' ) |
| 329 | ) { |
| 330 | $out->addHTML( |
| 331 | Html::warningBox( |
| 332 | $out->msg( 'move-over-sharedrepo', $newTitle->getPrefixedText() )->parse() |
| 333 | ) |
| 334 | ); |
| 335 | $moveOverShared = true; |
| 336 | $status = StatusValue::newGood(); |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | $oldTalk = $this->oldTitle->getTalkPageIfDefined(); |
| 341 | $oldTitleSubpages = $this->oldTitle->hasSubpages(); |
| 342 | $oldTitleTalkSubpages = $this->oldTitle->getTalkPageIfDefined()->hasSubpages(); |
| 343 | |
| 344 | $canMoveSubpage = ( $oldTitleSubpages || $oldTitleTalkSubpages ) && |
| 345 | $this->permManager->quickUserCan( |
| 346 | 'move-subpages', |
| 347 | $user, |
| 348 | $this->oldTitle |
| 349 | ); |
| 350 | |
| 351 | # We also want to be able to move assoc. subpage talk-pages even if base page |
| 352 | # has no associated talk page, so || with $oldTitleTalkSubpages. |
| 353 | $considerTalk = !$this->oldTitle->isTalkPage() && |
| 354 | ( $oldTalk->exists() |
| 355 | || ( $oldTitleTalkSubpages && $canMoveSubpage ) ); |
| 356 | |
| 357 | if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) { |
| 358 | $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
| 359 | ->select( '1' ) |
| 360 | ->from( 'redirect' ) |
| 361 | ->where( [ 'rd_namespace' => $this->oldTitle->getNamespace() ] ) |
| 362 | ->andWhere( [ 'rd_title' => $this->oldTitle->getDBkey() ] ) |
| 363 | ->andWhere( [ 'rd_interwiki' => '' ] ); |
| 364 | |
| 365 | $hasRedirects = (bool)$queryBuilder->caller( __METHOD__ )->fetchField(); |
| 366 | } else { |
| 367 | $hasRedirects = false; |
| 368 | } |
| 369 | |
| 370 | $messages = $status->getMessages(); |
| 371 | if ( $messages ) { |
| 372 | if ( $status instanceof PermissionStatus ) { |
| 373 | $action_desc = $this->msg( 'action-move' )->plain(); |
| 374 | $errMsgHtml = $this->msg( 'permissionserrorstext-withaction', |
| 375 | count( $messages ), $action_desc )->parseAsBlock(); |
| 376 | } else { |
| 377 | $errMsgHtml = $this->msg( 'cannotmove', count( $messages ) )->parseAsBlock(); |
| 378 | } |
| 379 | |
| 380 | if ( count( $messages ) == 1 ) { |
| 381 | $errMsgHtml .= $this->msg( $messages[0] )->parseAsBlock(); |
| 382 | } else { |
| 383 | $errStr = []; |
| 384 | |
| 385 | foreach ( $messages as $msg ) { |
| 386 | $errStr[] = $this->msg( $msg )->parse(); |
| 387 | } |
| 388 | |
| 389 | $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n"; |
| 390 | } |
| 391 | $out->addHTML( Html::errorBox( $errMsgHtml ) ); |
| 392 | } |
| 393 | |
| 394 | if ( $this->restrictionStore->isProtected( $this->oldTitle, 'move' ) ) { |
| 395 | # Is the title semi-protected? |
| 396 | if ( $this->restrictionStore->isSemiProtected( $this->oldTitle, 'move' ) ) { |
| 397 | $noticeMsg = 'semiprotectedpagemovewarning'; |
| 398 | } else { |
| 399 | # Then it must be protected based on static groups (regular) |
| 400 | $noticeMsg = 'protectedpagemovewarning'; |
| 401 | } |
| 402 | LogEventsList::showLogExtract( |
| 403 | $out, |
| 404 | 'protect', |
| 405 | $this->oldTitle, |
| 406 | '', |
| 407 | [ 'lim' => 1, 'msgKey' => $noticeMsg ] |
| 408 | ); |
| 409 | } |
| 410 | |
| 411 | // Length limit for wpReason and wpNewTitleMain is enforced in the |
| 412 | // mediawiki.special.movePage module |
| 413 | |
| 414 | $immovableNamespaces = []; |
| 415 | foreach ( $this->getLanguage()->getNamespaces() as $nsId => $_ ) { |
| 416 | if ( !$this->nsInfo->isMovable( $nsId ) ) { |
| 417 | $immovableNamespaces[] = $nsId; |
| 418 | } |
| 419 | } |
| 420 | |
| 421 | $out->enableOOUI(); |
| 422 | $fields = []; |
| 423 | |
| 424 | $fields[] = new FieldLayout( |
| 425 | new ComplexTitleInputWidget( [ |
| 426 | 'id' => 'wpNewTitle', |
| 427 | 'namespace' => [ |
| 428 | 'id' => 'wpNewTitleNs', |
| 429 | 'name' => 'wpNewTitleNs', |
| 430 | 'value' => $newTitle->getNamespace(), |
| 431 | 'exclude' => $immovableNamespaces, |
| 432 | ], |
| 433 | 'title' => [ |
| 434 | 'id' => 'wpNewTitleMain', |
| 435 | 'name' => 'wpNewTitleMain', |
| 436 | 'value' => $newTitle->getText(), |
| 437 | // Inappropriate, since we're expecting the user to input a non-existent page's title |
| 438 | 'suggestions' => false, |
| 439 | ], |
| 440 | 'infusable' => true, |
| 441 | ] ), |
| 442 | [ |
| 443 | 'label' => $this->msg( 'newtitle' )->text(), |
| 444 | 'align' => 'top', |
| 445 | ] |
| 446 | ); |
| 447 | |
| 448 | $options = Html::listDropdownOptions( |
| 449 | $this->msg( 'movepage-reason-dropdown' ) |
| 450 | ->page( $this->oldTitle ) |
| 451 | ->inContentLanguage() |
| 452 | ->text(), |
| 453 | [ 'other' => $this->msg( 'movereasonotherlist' )->text() ] |
| 454 | ); |
| 455 | $options = Html::listDropdownOptionsOoui( $options ); |
| 456 | |
| 457 | $fields[] = new FieldLayout( |
| 458 | new DropdownInputWidget( [ |
| 459 | 'name' => 'wpReasonList', |
| 460 | 'inputId' => 'wpReasonList', |
| 461 | 'infusable' => true, |
| 462 | 'value' => $this->getRequest()->getText( 'wpReasonList', 'other' ), |
| 463 | 'options' => $options, |
| 464 | ] ), |
| 465 | [ |
| 466 | 'label' => $this->msg( 'movereason' )->text(), |
| 467 | 'align' => 'top', |
| 468 | ] |
| 469 | ); |
| 470 | |
| 471 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
| 472 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
| 473 | // Unicode codepoints. |
| 474 | $fields[] = new FieldLayout( |
| 475 | new TextInputWidget( [ |
| 476 | 'name' => 'wpReason', |
| 477 | 'id' => 'wpReason', |
| 478 | 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
| 479 | 'infusable' => true, |
| 480 | 'value' => $this->getRequest()->getText( 'wpReason' ), |
| 481 | ] ), |
| 482 | [ |
| 483 | 'label' => $this->msg( 'moveotherreason' )->text(), |
| 484 | 'align' => 'top', |
| 485 | ] |
| 486 | ); |
| 487 | |
| 488 | if ( $considerTalk ) { |
| 489 | $fields[] = new FieldLayout( |
| 490 | new CheckboxInputWidget( [ |
| 491 | 'name' => 'wpMovetalk', |
| 492 | 'id' => 'wpMovetalk', |
| 493 | 'value' => '1', |
| 494 | 'selected' => $this->moveTalk, |
| 495 | ] ), |
| 496 | [ |
| 497 | 'label' => $this->msg( 'movetalk' )->text(), |
| 498 | 'help' => new HtmlSnippet( $this->msg( 'movepagetalktext' )->parseAsBlock() ), |
| 499 | 'helpInline' => true, |
| 500 | 'align' => 'inline', |
| 501 | 'id' => 'wpMovetalk-field', |
| 502 | ] |
| 503 | ); |
| 504 | } |
| 505 | |
| 506 | if ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) { |
| 507 | if ( $createRedirect ) { |
| 508 | $isChecked = $this->leaveRedirect; |
| 509 | $isDisabled = false; |
| 510 | } else { |
| 511 | $isChecked = false; |
| 512 | $isDisabled = true; |
| 513 | } |
| 514 | $fields[] = new FieldLayout( |
| 515 | new CheckboxInputWidget( [ |
| 516 | 'name' => 'wpLeaveRedirect', |
| 517 | 'id' => 'wpLeaveRedirect', |
| 518 | 'value' => '1', |
| 519 | 'selected' => $isChecked, |
| 520 | 'disabled' => $isDisabled, |
| 521 | ] ), |
| 522 | [ |
| 523 | 'label' => $this->msg( 'move-leave-redirect' )->text(), |
| 524 | 'align' => 'inline', |
| 525 | ] |
| 526 | ); |
| 527 | } |
| 528 | |
| 529 | if ( $hasRedirects ) { |
| 530 | $fields[] = new FieldLayout( |
| 531 | new CheckboxInputWidget( [ |
| 532 | 'name' => 'wpFixRedirects', |
| 533 | 'id' => 'wpFixRedirects', |
| 534 | 'value' => '1', |
| 535 | 'selected' => $this->fixRedirects, |
| 536 | ] ), |
| 537 | [ |
| 538 | 'label' => $this->msg( 'fix-double-redirects' )->text(), |
| 539 | 'align' => 'inline', |
| 540 | ] |
| 541 | ); |
| 542 | } |
| 543 | |
| 544 | if ( $canMoveSubpage ) { |
| 545 | $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
| 546 | $fields[] = new FieldLayout( |
| 547 | new CheckboxInputWidget( [ |
| 548 | 'name' => 'wpMovesubpages', |
| 549 | 'id' => 'wpMovesubpages', |
| 550 | 'value' => '1', |
| 551 | 'selected' => $this->moveSubpages, |
| 552 | ] ), |
| 553 | [ |
| 554 | 'label' => new HtmlSnippet( |
| 555 | $this->msg( |
| 556 | ( $this->oldTitle->hasSubpages() |
| 557 | ? 'move-subpages' |
| 558 | : 'move-talk-subpages' ) |
| 559 | )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse() |
| 560 | ), |
| 561 | 'align' => 'inline', |
| 562 | ] |
| 563 | ); |
| 564 | } |
| 565 | |
| 566 | # Don't allow watching if user is not logged in |
| 567 | if ( $user->isRegistered() ) { |
| 568 | $watchChecked = ( $this->watch || $this->userOptionsLookup->getBoolOption( $user, 'watchmoves' ) |
| 569 | || $this->watchlistManager->isWatched( $user, $this->oldTitle ) ); |
| 570 | $fields[] = new FieldLayout( |
| 571 | new CheckboxInputWidget( [ |
| 572 | 'name' => 'wpWatch', |
| 573 | 'id' => 'watch', # ew |
| 574 | 'infusable' => true, |
| 575 | 'value' => '1', |
| 576 | 'selected' => $watchChecked, |
| 577 | ] ), |
| 578 | [ |
| 579 | 'label' => $this->msg( 'move-watch' )->text(), |
| 580 | 'align' => 'inline', |
| 581 | ] |
| 582 | ); |
| 583 | |
| 584 | # Add a dropdown for watchlist expiry times in the form, T261230 |
| 585 | if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) ) { |
| 586 | $expiryOptions = WatchAction::getExpiryOptions( |
| 587 | $this->getContext(), |
| 588 | $this->watchedItemStore->getWatchedItem( $user, $this->oldTitle ) |
| 589 | ); |
| 590 | # Reformat the options to match what DropdownInputWidget wants. |
| 591 | $options = []; |
| 592 | foreach ( $expiryOptions['options'] as $label => $value ) { |
| 593 | $options[] = [ 'data' => $value, 'label' => $label ]; |
| 594 | } |
| 595 | |
| 596 | $fields[] = new FieldLayout( |
| 597 | new DropdownInputWidget( [ |
| 598 | 'name' => 'wpWatchlistExpiry', |
| 599 | 'id' => 'wpWatchlistExpiry', |
| 600 | 'infusable' => true, |
| 601 | 'options' => $options, |
| 602 | ] ), |
| 603 | [ |
| 604 | 'label' => $this->msg( 'confirm-watch-label' )->text(), |
| 605 | 'id' => 'wpWatchlistExpiryLabel', |
| 606 | 'infusable' => true, |
| 607 | 'align' => 'inline', |
| 608 | ] |
| 609 | ); |
| 610 | } |
| 611 | } |
| 612 | |
| 613 | $hiddenFields = ''; |
| 614 | if ( $moveOverShared ) { |
| 615 | $hiddenFields .= Html::hidden( 'wpMoveOverSharedFile', '1' ); |
| 616 | } |
| 617 | |
| 618 | if ( $deleteAndMove ) { |
| 619 | $fields[] = new FieldLayout( |
| 620 | new CheckboxInputWidget( [ |
| 621 | 'name' => 'wpDeleteAndMove', |
| 622 | 'id' => 'wpDeleteAndMove', |
| 623 | 'value' => '1', |
| 624 | ] ), |
| 625 | [ |
| 626 | 'label' => $this->msg( 'delete_and_move_confirm', $newTitle->getPrefixedText() )->text(), |
| 627 | 'align' => 'inline', |
| 628 | ] |
| 629 | ); |
| 630 | } |
| 631 | |
| 632 | $fields[] = new FieldLayout( |
| 633 | new ButtonInputWidget( [ |
| 634 | 'name' => 'wpMove', |
| 635 | 'value' => $this->msg( 'movepagebtn' )->text(), |
| 636 | 'label' => $this->msg( 'movepagebtn' )->text(), |
| 637 | 'flags' => [ 'primary', 'progressive' ], |
| 638 | 'type' => 'submit', |
| 639 | ] ), |
| 640 | [ |
| 641 | 'align' => 'top', |
| 642 | ] |
| 643 | ); |
| 644 | |
| 645 | $fieldset = new FieldsetLayout( [ |
| 646 | 'label' => $this->msg( 'move-page-legend' )->text(), |
| 647 | 'id' => 'mw-movepage-table', |
| 648 | 'items' => $fields, |
| 649 | ] ); |
| 650 | |
| 651 | $form = new FormLayout( [ |
| 652 | 'method' => 'post', |
| 653 | 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ), |
| 654 | 'id' => 'movepage', |
| 655 | ] ); |
| 656 | $form->appendContent( |
| 657 | $fieldset, |
| 658 | new HtmlSnippet( |
| 659 | $hiddenFields . |
| 660 | Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . |
| 661 | Html::hidden( 'wpEditToken', $user->getEditToken() ) |
| 662 | ) |
| 663 | ); |
| 664 | |
| 665 | $out->addHTML( |
| 666 | ( new PanelLayout( [ |
| 667 | 'classes' => [ 'movepage-wrapper' ], |
| 668 | 'expanded' => false, |
| 669 | 'padded' => true, |
| 670 | 'framed' => true, |
| 671 | 'content' => $form, |
| 672 | ] ) )->toString() |
| 673 | ); |
| 674 | if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) { |
| 675 | $link = $this->getLinkRenderer()->makeKnownLink( |
| 676 | $this->msg( 'movepage-reason-dropdown' )->inContentLanguage()->getTitle(), |
| 677 | $this->msg( 'movepage-edit-reasonlist' )->text(), |
| 678 | [], |
| 679 | [ 'action' => 'edit' ] |
| 680 | ); |
| 681 | $out->addHTML( Html::rawElement( 'p', [ 'class' => 'mw-movepage-editreasons' ], $link ) ); |
| 682 | } |
| 683 | |
| 684 | $this->showLogFragment( $this->oldTitle ); |
| 685 | $this->showSubpages( $this->oldTitle ); |
| 686 | } |
| 687 | |
| 688 | private function doSubmit() { |
| 689 | $user = $this->getUser(); |
| 690 | |
| 691 | if ( $user->pingLimiter( 'move' ) ) { |
| 692 | throw new ThrottledError; |
| 693 | } |
| 694 | |
| 695 | $ot = $this->oldTitle; |
| 696 | $nt = $this->newTitle; |
| 697 | |
| 698 | # don't allow moving to pages with # in |
| 699 | if ( !$nt || $nt->hasFragment() ) { |
| 700 | $this->showForm( StatusValue::newFatal( 'badtitletext' ) ); |
| 701 | |
| 702 | return; |
| 703 | } |
| 704 | |
| 705 | # Show a warning if the target file exists on a shared repo |
| 706 | if ( $nt->getNamespace() === NS_FILE |
| 707 | && !( $this->moveOverShared && $this->permManager->userHasRight( $user, 'reupload-shared' ) ) |
| 708 | && !$this->repoGroup->getLocalRepo()->findFile( $nt ) |
| 709 | && $this->repoGroup->findFile( $nt ) |
| 710 | ) { |
| 711 | $this->showForm( StatusValue::newFatal( 'file-exists-sharedrepo' ) ); |
| 712 | |
| 713 | return; |
| 714 | } |
| 715 | |
| 716 | # Delete to make way if requested |
| 717 | if ( $this->deleteAndMove ) { |
| 718 | $redir2 = $nt->isSingleRevRedirect(); |
| 719 | |
| 720 | $permStatus = $this->permManager->getPermissionStatus( |
| 721 | $redir2 ? 'delete-redirect' : 'delete', |
| 722 | $user, $nt |
| 723 | ); |
| 724 | if ( !$permStatus->isGood() ) { |
| 725 | if ( $redir2 ) { |
| 726 | if ( !$this->permManager->userCan( 'delete', $user, $nt ) ) { |
| 727 | // Cannot delete-redirect, or delete normally |
| 728 | $this->showForm( $permStatus ); |
| 729 | return; |
| 730 | } else { |
| 731 | // Cannot delete-redirect, but can delete normally, |
| 732 | // so log as a normal deletion |
| 733 | $redir2 = false; |
| 734 | } |
| 735 | } else { |
| 736 | // Cannot delete normally |
| 737 | $this->showForm( $permStatus ); |
| 738 | return; |
| 739 | } |
| 740 | } |
| 741 | |
| 742 | $page = $this->wikiPageFactory->newFromTitle( $nt ); |
| 743 | $delPage = $this->deletePageFactory->newDeletePage( $page, $user ); |
| 744 | |
| 745 | // Small safety margin to guard against concurrent edits |
| 746 | if ( $delPage->isBatchedDelete( 5 ) ) { |
| 747 | $this->showForm( StatusValue::newFatal( 'movepage-delete-first' ) ); |
| 748 | |
| 749 | return; |
| 750 | } |
| 751 | |
| 752 | $reason = $this->msg( 'delete_and_move_reason', $ot->getPrefixedText() )->inContentLanguage()->text(); |
| 753 | |
| 754 | // Delete an associated image if there is |
| 755 | if ( $nt->getNamespace() === NS_FILE ) { |
| 756 | $file = $this->repoGroup->getLocalRepo()->newFile( $nt ); |
| 757 | $file->load( IDBAccessObject::READ_LATEST ); |
| 758 | if ( $file->exists() ) { |
| 759 | $file->deleteFile( $reason, $user, false ); |
| 760 | } |
| 761 | } |
| 762 | |
| 763 | $deletionLog = $redir2 ? 'delete_redir2' : 'delete'; |
| 764 | $deleteStatus = $delPage |
| 765 | ->setLogSubtype( $deletionLog ) |
| 766 | // Should be redundant thanks to the isBatchedDelete check above. |
| 767 | ->forceImmediate( true ) |
| 768 | ->deleteUnsafe( $reason ); |
| 769 | |
| 770 | if ( !$deleteStatus->isGood() ) { |
| 771 | $this->showForm( $deleteStatus ); |
| 772 | |
| 773 | return; |
| 774 | } |
| 775 | } |
| 776 | |
| 777 | $handler = $this->contentHandlerFactory->getContentHandler( $ot->getContentModel() ); |
| 778 | |
| 779 | if ( !$handler->supportsRedirects() || ( |
| 780 | // Do not create redirects for wikitext message overrides (T376399). |
| 781 | // Maybe one day they will have a custom content model and this special case won't be needed. |
| 782 | $ot->getNamespace() === NS_MEDIAWIKI && |
| 783 | $ot->getContentModel() === CONTENT_MODEL_WIKITEXT |
| 784 | ) ) { |
| 785 | $createRedirect = false; |
| 786 | } elseif ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) { |
| 787 | $createRedirect = $this->leaveRedirect; |
| 788 | } else { |
| 789 | $createRedirect = true; |
| 790 | } |
| 791 | |
| 792 | # Do the actual move. |
| 793 | $mp = $this->movePageFactory->newMovePage( $ot, $nt ); |
| 794 | |
| 795 | if ( $ot->isTalkPage() || $nt->isTalkPage() ) { |
| 796 | $this->moveTalk = false; |
| 797 | } |
| 798 | if ( $this->moveSubpages ) { |
| 799 | $this->moveSubpages = $this->permManager->userCan( 'move-subpages', $user, $ot ); |
| 800 | } |
| 801 | |
| 802 | # check whether the requested actions are permitted / possible |
| 803 | $permStatus = $mp->authorizeMove( $this->getAuthority(), $this->reason ); |
| 804 | if ( !$permStatus->isOK() ) { |
| 805 | $this->showForm( $permStatus ); |
| 806 | return; |
| 807 | } |
| 808 | $status = $mp->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect ); |
| 809 | if ( !$status->isOK() ) { |
| 810 | $this->showForm( $status ); |
| 811 | return; |
| 812 | } |
| 813 | |
| 814 | if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) && $this->fixRedirects ) { |
| 815 | DoubleRedirectJob::fixRedirects( 'move', $ot ); |
| 816 | } |
| 817 | |
| 818 | $out = $this->getOutput(); |
| 819 | $out->setPageTitleMsg( $this->msg( 'pagemovedsub' ) ); |
| 820 | |
| 821 | $linkRenderer = $this->getLinkRenderer(); |
| 822 | $oldLink = $linkRenderer->makeLink( |
| 823 | $ot, |
| 824 | null, |
| 825 | [ 'id' => 'movepage-oldlink' ], |
| 826 | [ 'redirect' => 'no' ] |
| 827 | ); |
| 828 | $newLink = $linkRenderer->makeKnownLink( |
| 829 | $nt, |
| 830 | null, |
| 831 | [ 'id' => 'movepage-newlink' ] |
| 832 | ); |
| 833 | $oldText = $ot->getPrefixedText(); |
| 834 | $newText = $nt->getPrefixedText(); |
| 835 | |
| 836 | $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink, |
| 837 | $newLink )->params( $oldText, $newText )->parseAsBlock() ); |
| 838 | $out->addWikiMsg( isset( $status->getValue()['redirectRevision'] ) ? |
| 839 | 'movepage-moved-redirect' : |
| 840 | 'movepage-moved-noredirect' ); |
| 841 | |
| 842 | $this->getHookRunner()->onSpecialMovepageAfterMove( $this, $ot, $nt ); |
| 843 | |
| 844 | /* |
| 845 | * Now we move extra pages we've been asked to move: subpages and talk |
| 846 | * pages. |
| 847 | * |
| 848 | * First, make a list of id's. This might be marginally less efficient |
| 849 | * than a more direct method, but this is not a highly performance-cri- |
| 850 | * tical code path and readable code is more important here. |
| 851 | * |
| 852 | * If the target namespace doesn't allow subpages, moving with subpages |
| 853 | * would mean that you couldn't move them back in one operation, which |
| 854 | * is bad. |
| 855 | * @todo FIXME: A specific error message should be given in this case. |
| 856 | */ |
| 857 | |
| 858 | // @todo FIXME: Use MovePage::moveSubpages() here |
| 859 | $dbr = $this->dbProvider->getReplicaDatabase(); |
| 860 | if ( $this->moveSubpages && ( |
| 861 | $this->nsInfo->hasSubpages( $nt->getNamespace() ) || ( |
| 862 | $this->moveTalk |
| 863 | && $this->nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() ) |
| 864 | ) |
| 865 | ) ) { |
| 866 | $conds = [ |
| 867 | $dbr->expr( |
| 868 | 'page_title', |
| 869 | IExpression::LIKE, |
| 870 | new LikeValue( $ot->getDBkey() . '/', $dbr->anyString() ) |
| 871 | )->or( 'page_title', '=', $ot->getDBkey() ) |
| 872 | ]; |
| 873 | $conds['page_namespace'] = []; |
| 874 | if ( $this->nsInfo->hasSubpages( $nt->getNamespace() ) ) { |
| 875 | $conds['page_namespace'][] = $ot->getNamespace(); |
| 876 | } |
| 877 | if ( $this->moveTalk && |
| 878 | $this->nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() ) |
| 879 | ) { |
| 880 | $conds['page_namespace'][] = $ot->getTalkPage()->getNamespace(); |
| 881 | } |
| 882 | } elseif ( $this->moveTalk ) { |
| 883 | $conds = [ |
| 884 | 'page_namespace' => $ot->getTalkPage()->getNamespace(), |
| 885 | 'page_title' => $ot->getDBkey() |
| 886 | ]; |
| 887 | } else { |
| 888 | # Skip the query |
| 889 | $conds = null; |
| 890 | } |
| 891 | |
| 892 | $extraPages = []; |
| 893 | if ( $conds !== null ) { |
| 894 | $extraPages = $this->titleFactory->newTitleArrayFromResult( |
| 895 | $dbr->newSelectQueryBuilder() |
| 896 | ->select( [ 'page_id', 'page_namespace', 'page_title' ] ) |
| 897 | ->from( 'page' ) |
| 898 | ->where( $conds ) |
| 899 | ->caller( __METHOD__ )->fetchResultSet() |
| 900 | ); |
| 901 | } |
| 902 | |
| 903 | $extraOutput = []; |
| 904 | $count = 1; |
| 905 | foreach ( $extraPages as $oldSubpage ) { |
| 906 | if ( $ot->equals( $oldSubpage ) || $nt->equals( $oldSubpage ) ) { |
| 907 | # Already did this one. |
| 908 | continue; |
| 909 | } |
| 910 | |
| 911 | $newPageName = preg_replace( |
| 912 | '#^' . preg_quote( $ot->getDBkey(), '#' ) . '#', |
| 913 | StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234 |
| 914 | $oldSubpage->getDBkey() |
| 915 | ); |
| 916 | |
| 917 | if ( $oldSubpage->isSubpage() && ( $ot->isTalkPage() xor $nt->isTalkPage() ) ) { |
| 918 | // Moving a subpage from a subject namespace to a talk namespace or vice-versa |
| 919 | $newNs = $nt->getNamespace(); |
| 920 | } elseif ( $oldSubpage->isTalkPage() ) { |
| 921 | $newNs = $nt->getTalkPage()->getNamespace(); |
| 922 | } else { |
| 923 | $newNs = $nt->getSubjectPage()->getNamespace(); |
| 924 | } |
| 925 | |
| 926 | # T16385: we need makeTitleSafe because the new page names may |
| 927 | # be longer than 255 characters. |
| 928 | $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); |
| 929 | if ( !$newSubpage ) { |
| 930 | $oldLink = $linkRenderer->makeKnownLink( $oldSubpage ); |
| 931 | $extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink ) |
| 932 | ->params( Title::makeName( $newNs, $newPageName ) )->escaped(); |
| 933 | continue; |
| 934 | } |
| 935 | |
| 936 | $mp = $this->movePageFactory->newMovePage( $oldSubpage, $newSubpage ); |
| 937 | # This was copy-pasted from Renameuser, bleh. |
| 938 | if ( $newSubpage->exists() && !$mp->isValidMove()->isOK() ) { |
| 939 | $link = $linkRenderer->makeKnownLink( $newSubpage ); |
| 940 | $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped(); |
| 941 | } else { |
| 942 | $status = $mp->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect ); |
| 943 | |
| 944 | if ( $status->isOK() ) { |
| 945 | if ( $this->fixRedirects ) { |
| 946 | DoubleRedirectJob::fixRedirects( 'move', $oldSubpage ); |
| 947 | } |
| 948 | $oldLink = $linkRenderer->makeLink( |
| 949 | $oldSubpage, |
| 950 | null, |
| 951 | [], |
| 952 | [ 'redirect' => 'no' ] |
| 953 | ); |
| 954 | |
| 955 | $newLink = $linkRenderer->makeKnownLink( $newSubpage ); |
| 956 | $extraOutput[] = $this->msg( 'movepage-page-moved' ) |
| 957 | ->rawParams( $oldLink, $newLink )->escaped(); |
| 958 | ++$count; |
| 959 | |
| 960 | $maximumMovedPages = |
| 961 | $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
| 962 | if ( $count >= $maximumMovedPages ) { |
| 963 | $extraOutput[] = $this->msg( 'movepage-max-pages' ) |
| 964 | ->numParams( $maximumMovedPages )->escaped(); |
| 965 | break; |
| 966 | } |
| 967 | } else { |
| 968 | $oldLink = $linkRenderer->makeKnownLink( $oldSubpage ); |
| 969 | $newLink = $linkRenderer->makeLink( $newSubpage ); |
| 970 | $extraOutput[] = $this->msg( 'movepage-page-unmoved' ) |
| 971 | ->rawParams( $oldLink, $newLink )->escaped(); |
| 972 | } |
| 973 | } |
| 974 | } |
| 975 | |
| 976 | if ( $extraOutput !== [] ) { |
| 977 | $out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" ); |
| 978 | } |
| 979 | |
| 980 | # Deal with watches (we don't watch subpages) |
| 981 | # Get text from expiry selection dropdown, T261230 |
| 982 | $expiry = $this->getRequest()->getText( 'wpWatchlistExpiry' ); |
| 983 | if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && $expiry !== '' ) { |
| 984 | $expiry = ExpiryDef::normalizeExpiry( $expiry, TS::ISO_8601 ); |
| 985 | } else { |
| 986 | $expiry = null; |
| 987 | } |
| 988 | |
| 989 | $this->watchlistManager->setWatch( |
| 990 | $this->watch, |
| 991 | $this->getAuthority(), |
| 992 | $ot, |
| 993 | $expiry |
| 994 | ); |
| 995 | |
| 996 | $this->watchlistManager->setWatch( |
| 997 | $this->watch, |
| 998 | $this->getAuthority(), |
| 999 | $nt, |
| 1000 | $expiry |
| 1001 | ); |
| 1002 | } |
| 1003 | |
| 1004 | private function showLogFragment( Title $title ) { |
| 1005 | $moveLogPage = new LogPage( 'move' ); |
| 1006 | $out = $this->getOutput(); |
| 1007 | $out->addHTML( Html::element( 'h2', [], $moveLogPage->getName()->text() ) ); |
| 1008 | LogEventsList::showLogExtract( $out, 'move', $title ); |
| 1009 | } |
| 1010 | |
| 1011 | /** |
| 1012 | * Show subpages of the page being moved. Section is not shown if both current |
| 1013 | * namespace does not support subpages and no talk subpages were found. |
| 1014 | * |
| 1015 | * @param Title $title Page being moved. |
| 1016 | */ |
| 1017 | private function showSubpages( $title ) { |
| 1018 | $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
| 1019 | $nsHasSubpages = $this->nsInfo->hasSubpages( $title->getNamespace() ); |
| 1020 | $subpages = $title->getSubpages( $maximumMovedPages + 1 ); |
| 1021 | $count = $subpages instanceof TitleArrayFromResult ? $subpages->count() : 0; |
| 1022 | |
| 1023 | $titleIsTalk = $title->isTalkPage(); |
| 1024 | $subpagesTalk = $title->getTalkPage()->getSubpages( $maximumMovedPages + 1 ); |
| 1025 | $countTalk = $subpagesTalk instanceof TitleArrayFromResult ? $subpagesTalk->count() : 0; |
| 1026 | $totalCount = $count + $countTalk; |
| 1027 | |
| 1028 | if ( !$nsHasSubpages && $countTalk == 0 ) { |
| 1029 | return; |
| 1030 | } |
| 1031 | |
| 1032 | $this->getOutput()->wrapWikiMsg( |
| 1033 | '== $1 ==', |
| 1034 | [ 'movesubpage', ( $titleIsTalk ? $count : $totalCount ) ] |
| 1035 | ); |
| 1036 | |
| 1037 | if ( $nsHasSubpages ) { |
| 1038 | $this->showSubpagesList( |
| 1039 | $subpages, $count, 'movesubpagetext', 'movesubpagetext-truncated', true |
| 1040 | ); |
| 1041 | } |
| 1042 | |
| 1043 | if ( !$titleIsTalk && $countTalk > 0 ) { |
| 1044 | $this->showSubpagesList( |
| 1045 | $subpagesTalk, $countTalk, 'movesubpagetalktext', 'movesubpagetalktext-truncated' |
| 1046 | ); |
| 1047 | } |
| 1048 | } |
| 1049 | |
| 1050 | private function showSubpagesList( |
| 1051 | TitleArrayFromResult $subpages, int $pagecount, string $msg, string $truncatedMsg, bool $noSubpageMsg = false |
| 1052 | ) { |
| 1053 | $out = $this->getOutput(); |
| 1054 | |
| 1055 | # No subpages. |
| 1056 | if ( $pagecount == 0 && $noSubpageMsg ) { |
| 1057 | $out->addWikiMsg( 'movenosubpage' ); |
| 1058 | return; |
| 1059 | } |
| 1060 | |
| 1061 | $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
| 1062 | |
| 1063 | if ( $pagecount > $maximumMovedPages ) { |
| 1064 | $subpages = $this->truncateSubpagesList( $subpages ); |
| 1065 | $out->addWikiMsg( $truncatedMsg, $this->getLanguage()->formatNum( $maximumMovedPages ) ); |
| 1066 | } else { |
| 1067 | $out->addWikiMsg( $msg, $this->getLanguage()->formatNum( $pagecount ) ); |
| 1068 | } |
| 1069 | $out->addHTML( "<ul>\n" ); |
| 1070 | |
| 1071 | $this->linkBatchFactory->newLinkBatch( $subpages ) |
| 1072 | ->setCaller( __METHOD__ ) |
| 1073 | ->execute(); |
| 1074 | $linkRenderer = $this->getLinkRenderer(); |
| 1075 | |
| 1076 | foreach ( $subpages as $subpage ) { |
| 1077 | $link = $linkRenderer->makeLink( $subpage ); |
| 1078 | $out->addHTML( "<li>$link</li>\n" ); |
| 1079 | } |
| 1080 | $out->addHTML( "</ul>\n" ); |
| 1081 | } |
| 1082 | |
| 1083 | private function truncateSubpagesList( iterable $subpages ): array { |
| 1084 | $returnArray = []; |
| 1085 | foreach ( $subpages as $subpage ) { |
| 1086 | $returnArray[] = $subpage; |
| 1087 | if ( count( $returnArray ) >= $this->getConfig()->get( MainConfigNames::MaximumMovedPages ) ) { |
| 1088 | break; |
| 1089 | } |
| 1090 | } |
| 1091 | return $returnArray; |
| 1092 | } |
| 1093 | |
| 1094 | /** |
| 1095 | * Return an array of subpages beginning with $search that this special page will accept. |
| 1096 | * |
| 1097 | * @param string $search Prefix to search for |
| 1098 | * @param int $limit Maximum number of results to return (usually 10) |
| 1099 | * @param int $offset Number of results to skip (usually 0) |
| 1100 | * @return string[] Matching subpages |
| 1101 | */ |
| 1102 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
| 1103 | return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory ); |
| 1104 | } |
| 1105 | |
| 1106 | /** @inheritDoc */ |
| 1107 | protected function getGroupName() { |
| 1108 | return 'pagetools'; |
| 1109 | } |
| 1110 | } |
| 1111 | |
| 1112 | /** |
| 1113 | * Retain the old class name for backwards compatibility. |
| 1114 | * @deprecated since 1.40 |
| 1115 | */ |
| 1116 | class_alias( SpecialMovePage::class, 'MovePageForm' ); |