MediaWiki  master
MovePage.php
Go to the documentation of this file.
1 <?php
2 
35 
42 class MovePage {
43 
47  protected $oldTitle;
48 
52  protected $newTitle;
53 
57  protected $options;
58 
62  protected $loadBalancer;
63 
67  protected $nsInfo;
68 
72  protected $watchedItems;
73 
77  protected $permMgr;
78 
82  protected $repoGroup;
83 
88 
92  private $revisionStore;
93 
97  private $spamChecker;
98 
102  private $hookContainer;
103 
107  private $hookRunner;
108 
112  public const CONSTRUCTOR_OPTIONS = [
113  'CategoryCollation'
114  ];
115 
132  public function __construct(
135  ServiceOptions $options = null,
137  NamespaceInfo $nsInfo = null,
140  RepoGroup $repoGroup = null,
143  SpamChecker $spamChecker = null,
145  ) {
146  $this->oldTitle = $oldTitle;
147  $this->newTitle = $newTitle;
148 
149  $services = MediaWikiServices::getInstance();
150  $this->options = $options ??
151  new ServiceOptions(
152  self::CONSTRUCTOR_OPTIONS,
153  $services->getMainConfig()
154  );
155  $this->loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
156  $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
157  $this->watchedItems = $watchedItems ?? $services->getWatchedItemStore();
158  $this->permMgr = $permMgr ?? $services->getPermissionManager();
159  $this->repoGroup = $repoGroup ?? $services->getRepoGroup();
160  $this->contentHandlerFactory =
161  $contentHandlerFactory ?? $services->getContentHandlerFactory();
162 
163  $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
164  $this->spamChecker = $spamChecker ?? $services->getSpamChecker();
165  $this->hookContainer = $hookContainer ?? $services->getHookContainer();
166  $this->hookRunner = new HookRunner( $this->hookContainer );
167  }
168 
177  public function checkPermissions( User $user, $reason ) {
178  $status = new Status();
179 
180  $errors = wfMergeErrorArrays(
181  $this->permMgr->getPermissionErrors( 'move', $user, $this->oldTitle ),
182  $this->permMgr->getPermissionErrors( 'edit', $user, $this->oldTitle ),
183  $this->permMgr->getPermissionErrors( 'move-target', $user, $this->newTitle ),
184  $this->permMgr->getPermissionErrors( 'edit', $user, $this->newTitle )
185  );
186 
187  // Convert into a Status object
188  if ( $errors ) {
189  foreach ( $errors as $error ) {
190  $status->fatal( ...$error );
191  }
192  }
193 
194  if ( $reason !== null && $this->spamChecker->checkSummary( $reason ) !== false ) {
195  // This is kind of lame, won't display nice
196  $status->fatal( 'spamprotectiontext' );
197  }
198 
199  $tp = $this->newTitle->getTitleProtection();
200  if ( $tp !== false && !$this->permMgr->userHasRight( $user, $tp['permission'] ) ) {
201  $status->fatal( 'cantmove-titleprotected' );
202  }
203 
204  $this->hookRunner->onMovePageCheckPermissions(
205  $this->oldTitle, $this->newTitle, $user, $reason, $status );
206 
207  return $status;
208  }
209 
217  public function isValidMove() {
218  $status = new Status();
219 
220  if ( $this->oldTitle->equals( $this->newTitle ) ) {
221  $status->fatal( 'selfmove' );
222  } elseif ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) {
223  // The move is allowed only if (1) the target doesn't exist, or (2) the target is a
224  // redirect to the source, and has no history (so we can undo bad moves right after
225  // they're done). If the target is a single revision redirect to a different page,
226  // it can be deleted with just `delete-redirect` rights (i.e. without needing
227  // `delete`) - see T239277
228  $fatal = $this->newTitle->isSingleRevRedirect() ? 'redirectexists' : 'articleexists';
229  $status->fatal( $fatal, $this->newTitle->getPrefixedText() );
230  }
231 
232  // @todo If the old title is invalid, maybe we should check if it somehow exists in the
233  // database and allow moving it to a valid name? Why prohibit the move from an empty name
234  // without checking in the database?
235  if ( $this->oldTitle->getDBkey() == '' ) {
236  $status->fatal( 'badarticleerror' );
237  } elseif ( $this->oldTitle->isExternal() ) {
238  $status->fatal( 'immobile-source-namespace-iw' );
239  } elseif ( !$this->oldTitle->isMovable() ) {
240  $nsText = $this->oldTitle->getNsText();
241  if ( $nsText === '' ) {
242  $nsText = wfMessage( 'blanknamespace' )->text();
243  }
244  $status->fatal( 'immobile-source-namespace', $nsText );
245  } elseif ( !$this->oldTitle->exists() ) {
246  $status->fatal( 'movepage-source-doesnt-exist' );
247  }
248 
249  if ( $this->newTitle->isExternal() ) {
250  $status->fatal( 'immobile-target-namespace-iw' );
251  } elseif ( !$this->newTitle->isMovable() ) {
252  $nsText = $this->newTitle->getNsText();
253  if ( $nsText === '' ) {
254  $nsText = wfMessage( 'blanknamespace' )->text();
255  }
256  $status->fatal( 'immobile-target-namespace', $nsText );
257  }
258  if ( !$this->newTitle->isValid() ) {
259  $status->fatal( 'movepage-invalid-target-title' );
260  }
261 
262  // Content model checks
263  if ( !$this->contentHandlerFactory
264  ->getContentHandler( $this->oldTitle->getContentModel() )
265  ->canBeUsedOn( $this->newTitle )
266  ) {
267  $status->fatal(
268  'content-not-allowed-here',
269  ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
270  $this->newTitle->getPrefixedText(),
271  SlotRecord::MAIN
272  );
273  }
274 
275  // Image-specific checks
276  if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
277  $status->merge( $this->isValidFileMove() );
278  }
279 
280  if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
281  $status->fatal( 'nonfile-cannot-move-to-file' );
282  }
283 
284  // Hook for extensions to say a title can't be moved for technical reasons
285  $this->hookRunner->onMovePageIsValidMove( $this->oldTitle, $this->newTitle, $status );
286 
287  return $status;
288  }
289 
295  protected function isValidFileMove() {
296  $status = new Status();
297 
298  if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
299  $status->fatal( 'imagenocrossnamespace' );
300  // No need for further errors about the target filename being wrong
301  return $status;
302  }
303 
304  $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
305  $file->load( File::READ_LATEST );
306  if ( $file->exists() ) {
307  if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) {
308  $status->fatal( 'imageinvalidfilename' );
309  }
310  if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
311  $status->fatal( 'imagetypemismatch' );
312  }
313  }
314 
315  return $status;
316  }
317 
325  protected function isValidMoveTarget() {
326  # Is it an existing file?
327  if ( $this->newTitle->inNamespace( NS_FILE ) ) {
328  $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
329  $file->load( File::READ_LATEST );
330  if ( $file->exists() ) {
331  wfDebug( __METHOD__ . ": file exists" );
332  return false;
333  }
334  }
335  # Is it a redirect with no history?
336  if ( !$this->newTitle->isSingleRevRedirect() ) {
337  wfDebug( __METHOD__ . ": not a one-rev redirect" );
338  return false;
339  }
340  # Get the article text
341  $rev = $this->revisionStore->getRevisionByTitle(
342  $this->newTitle,
343  0,
344  RevisionStore::READ_LATEST
345  );
346  if ( !is_object( $rev ) ) {
347  return false;
348  }
349  $content = $rev->getContent( SlotRecord::MAIN );
350  # Does the redirect point to the source?
351  # Or is it a broken self-redirect, usually caused by namespace collisions?
352  $redirTitle = $content ? $content->getRedirectTarget() : null;
353 
354  if ( $redirTitle ) {
355  if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
356  $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
357  wfDebug( __METHOD__ . ": redirect points to other page" );
358  return false;
359  } else {
360  return true;
361  }
362  } else {
363  # Fail safe (not a redirect after all. strange.)
364  wfDebug( __METHOD__ . ": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
365  " is a redirect, but it doesn't contain a valid redirect." );
366  return false;
367  }
368  }
369 
381  public function move(
382  User $user, $reason = null, $createRedirect = true, array $changeTags = []
383  ) {
384  $status = $this->isValidMove();
385  if ( !$status->isOK() ) {
386  return $status;
387  }
388 
389  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
390  }
391 
401  public function moveIfAllowed(
402  User $user, $reason = null, $createRedirect = true, array $changeTags = []
403  ) {
404  $status = $this->isValidMove();
405  $status->merge( $this->checkPermissions( $user, $reason ) );
406  if ( $changeTags ) {
407  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $user ) );
408  }
409 
410  if ( !$status->isOK() ) {
411  // Auto-block user's IP if the account was "hard" blocked
412  $user->spreadAnyEditBlock();
413  return $status;
414  }
415 
416  // Check suppressredirect permission
417  if ( !$this->permMgr->userHasRight( $user, 'suppressredirect' ) ) {
418  $createRedirect = true;
419  }
420 
421  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
422  }
423 
438  public function moveSubpages(
439  User $user, $reason = null, $createRedirect = true, array $changeTags = []
440  ) {
441  return $this->moveSubpagesInternal( false, $user, $reason, $createRedirect, $changeTags );
442  }
443 
457  public function moveSubpagesIfAllowed(
458  User $user, $reason = null, $createRedirect = true, array $changeTags = []
459  ) {
460  return $this->moveSubpagesInternal( true, $user, $reason, $createRedirect, $changeTags );
461  }
462 
471  private function moveSubpagesInternal(
472  $checkPermissions, User $user, $reason, $createRedirect, array $changeTags
473  ) {
474  global $wgMaximumMovedPages;
475 
476  if ( $checkPermissions ) {
477  if ( !$this->permMgr->userCan(
478  'move-subpages', $user, $this->oldTitle )
479  ) {
480  return Status::newFatal( 'cant-move-subpages' );
481  }
482  }
483 
484  // Do the source and target namespaces support subpages?
485  if ( !$this->nsInfo->hasSubpages( $this->oldTitle->getNamespace() ) ) {
486  return Status::newFatal( 'namespace-nosubpages',
487  $this->nsInfo->getCanonicalName( $this->oldTitle->getNamespace() ) );
488  }
489  if ( !$this->nsInfo->hasSubpages( $this->newTitle->getNamespace() ) ) {
490  return Status::newFatal( 'namespace-nosubpages',
491  $this->nsInfo->getCanonicalName( $this->newTitle->getNamespace() ) );
492  }
493 
494  // Return a status for the overall result. Its value will be an array with per-title
495  // status for each subpage. Merge any errors from the per-title statuses into the
496  // top-level status without resetting the overall result.
497  $topStatus = Status::newGood();
498  $perTitleStatus = [];
499  $subpages = $this->oldTitle->getSubpages( $wgMaximumMovedPages + 1 );
500  $count = 0;
501  foreach ( $subpages as $oldSubpage ) {
502  $count++;
503  if ( $count > $wgMaximumMovedPages ) {
504  $status = Status::newFatal( 'movepage-max-pages', $wgMaximumMovedPages );
505  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
506  $topStatus->merge( $status );
507  $topStatus->setOK( true );
508  break;
509  }
510 
511  // We don't know whether this function was called before or after moving the root page,
512  // so check both titles
513  if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
514  $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
515  ) {
516  // When moving a page to a subpage of itself, don't move it twice
517  continue;
518  }
519  $newPageName = preg_replace(
520  '#^' . preg_quote( $this->oldTitle->getDBkey(), '#' ) . '#',
521  StringUtils::escapeRegexReplacement( $this->newTitle->getDBkey() ), # T23234
522  $oldSubpage->getDBkey() );
523  if ( $oldSubpage->isTalkPage() ) {
524  $newNs = $this->newTitle->getTalkPage()->getNamespace();
525  } else {
526  $newNs = $this->newTitle->getSubjectPage()->getNamespace();
527  }
528  // T16385: we need makeTitleSafe because the new page names may be longer than 255
529  // characters.
530  $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
531 
532  $mp = new MovePage( $oldSubpage, $newSubpage );
533  $method = $checkPermissions ? 'moveIfAllowed' : 'move';
535  $status = $mp->$method( $user, $reason, $createRedirect, $changeTags );
536  if ( $status->isOK() ) {
537  $status->setResult( true, $newSubpage->getPrefixedText() );
538  }
539  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
540  $topStatus->merge( $status );
541  $topStatus->setOK( true );
542  }
543 
544  $topStatus->value = $perTitleStatus;
545  return $topStatus;
546  }
547 
557  private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) {
558  $status = Status::newGood();
559  $this->hookRunner->onTitleMove( $this->oldTitle, $this->newTitle, $user, $reason, $status );
560  if ( !$status->isOK() ) {
561  // Move was aborted by the hook
562  return $status;
563  }
564 
565  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
566  $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
567 
568  $this->hookRunner->onTitleMoveStarting( $this->oldTitle, $this->newTitle, $user );
569 
570  $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
571  $protected = $this->oldTitle->isProtected();
572 
573  // Do the actual move; if this fails, it will throw an MWException(!)
574  $nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
575  $changeTags );
576 
577  // Refresh the sortkey for this row. Be careful to avoid resetting
578  // cl_timestamp, which may disturb time-based lists on some sites.
579  // @todo This block should be killed, it's duplicating code
580  // from LinksUpdate::getCategoryInsertions() and friends.
581  $prefixes = $dbw->select(
582  'categorylinks',
583  [ 'cl_sortkey_prefix', 'cl_to' ],
584  [ 'cl_from' => $pageid ],
585  __METHOD__
586  );
587  $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() );
588  foreach ( $prefixes as $prefixRow ) {
589  $prefix = $prefixRow->cl_sortkey_prefix;
590  $catTo = $prefixRow->cl_to;
591  $dbw->update( 'categorylinks',
592  [
593  'cl_sortkey' => Collation::singleton()->getSortKey(
594  $this->newTitle->getCategorySortkey( $prefix ) ),
595  'cl_collation' => $this->options->get( 'CategoryCollation' ),
596  'cl_type' => $type,
597  'cl_timestamp=cl_timestamp' ],
598  [
599  'cl_from' => $pageid,
600  'cl_to' => $catTo ],
601  __METHOD__
602  );
603  }
604 
605  $redirid = $this->oldTitle->getArticleID();
606 
607  if ( $protected ) {
608  # Protect the redirect title as the title used to be...
609  $res = $dbw->select(
610  'page_restrictions',
611  [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
612  [ 'pr_page' => $pageid ],
613  __METHOD__,
614  'FOR UPDATE'
615  );
616  $rowsInsert = [];
617  foreach ( $res as $row ) {
618  $rowsInsert[] = [
619  'pr_page' => $redirid,
620  'pr_type' => $row->pr_type,
621  'pr_level' => $row->pr_level,
622  'pr_cascade' => $row->pr_cascade,
623  'pr_user' => $row->pr_user,
624  'pr_expiry' => $row->pr_expiry
625  ];
626  }
627  $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
628 
629  // Build comment for log
630  $comment = wfMessage(
631  'prot_1movedto2',
632  $this->oldTitle->getPrefixedText(),
633  $this->newTitle->getPrefixedText()
634  )->inContentLanguage()->text();
635  if ( $reason ) {
636  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
637  }
638 
639  // reread inserted pr_ids for log relation
640  $insertedPrIds = $dbw->select(
641  'page_restrictions',
642  'pr_id',
643  [ 'pr_page' => $redirid ],
644  __METHOD__
645  );
646  $logRelationsValues = [];
647  foreach ( $insertedPrIds as $prid ) {
648  $logRelationsValues[] = $prid->pr_id;
649  }
650 
651  // Update the protection log
652  $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
653  $logEntry->setTarget( $this->newTitle );
654  $logEntry->setComment( $comment );
655  $logEntry->setPerformer( $user );
656  $logEntry->setParameters( [
657  '4::oldtitle' => $this->oldTitle->getPrefixedText(),
658  ] );
659  $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
660  $logEntry->addTags( $changeTags );
661  $logId = $logEntry->insert();
662  $logEntry->publish( $logId );
663  }
664 
665  // Update *_from_namespace fields as needed
666  if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
667  $dbw->update( 'pagelinks',
668  [ 'pl_from_namespace' => $this->newTitle->getNamespace() ],
669  [ 'pl_from' => $pageid ],
670  __METHOD__
671  );
672  $dbw->update( 'templatelinks',
673  [ 'tl_from_namespace' => $this->newTitle->getNamespace() ],
674  [ 'tl_from' => $pageid ],
675  __METHOD__
676  );
677  $dbw->update( 'imagelinks',
678  [ 'il_from_namespace' => $this->newTitle->getNamespace() ],
679  [ 'il_from' => $pageid ],
680  __METHOD__
681  );
682  }
683 
684  # Update watchlists
685  $oldtitle = $this->oldTitle->getDBkey();
686  $newtitle = $this->newTitle->getDBkey();
687  $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
688  $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
689  if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
690  $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
691  }
692 
693  // If it is a file then move it last.
694  // This is done after all database changes so that file system errors cancel the transaction.
695  if ( $this->oldTitle->getNamespace() === NS_FILE ) {
696  $status = $this->moveFile( $this->oldTitle, $this->newTitle );
697  if ( !$status->isOK() ) {
698  $dbw->cancelAtomic( __METHOD__ );
699  return $status;
700  }
701  }
702 
703  $this->hookRunner->onPageMoveCompleting(
704  $this->oldTitle, $this->newTitle,
705  $user, $pageid, $redirid, $reason, $nullRevision
706  );
707 
708  // Deprecated since 1.35, use PageMoveCompleting
709  if ( $this->hookContainer->isRegistered( 'TitleMoveCompleting' ) ) {
710  // Only create the Revision object if needed
711  $nullRevisionObj = new Revision( $nullRevision );
712  $this->hookRunner->onTitleMoveCompleting(
713  $this->oldTitle,
714  $this->newTitle,
715  $user,
716  $pageid,
717  $redirid,
718  $reason,
719  $nullRevisionObj
720  );
721  }
722 
723  $dbw->endAtomic( __METHOD__ );
724 
725  // Keep each single hook handler atomic
728  $dbw,
729  __METHOD__,
730  function () use ( $user, $pageid, $redirid, $reason, $nullRevision ) {
731  $this->hookRunner->onPageMoveComplete(
732  $this->oldTitle,
733  $this->newTitle,
734  $user,
735  $pageid,
736  $redirid,
737  $reason,
738  $nullRevision
739  );
740 
741  if ( !$this->hookContainer->isRegistered( 'TitleMoveComplete' ) ) {
742  // Don't go on to create a Revision unless its needed
743  return;
744  }
745 
746  $nullRevisionObj = new Revision( $nullRevision );
747  // Deprecated since 1.35, use PageMoveComplete
748  $this->hookRunner->onTitleMoveComplete(
749  $this->oldTitle,
750  $this->newTitle,
751  $user, $pageid,
752  $redirid,
753  $reason,
754  $nullRevisionObj
755  );
756  }
757  )
758  );
759 
760  return Status::newGood();
761  }
762 
772  private function moveFile( $oldTitle, $newTitle ) {
773  $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle );
774  $file->load( File::READ_LATEST );
775  if ( $file->exists() ) {
776  $status = $file->move( $newTitle );
777  } else {
778  $status = Status::newGood();
779  }
780 
781  // Clear RepoGroup process cache
782  $this->repoGroup->clearCache( $oldTitle );
783  $this->repoGroup->clearCache( $newTitle ); # clear false negative cache
784  return $status;
785  }
786 
802  private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true,
803  array $changeTags = []
804  ) {
805  if ( $nt->exists() ) {
806  $moveOverRedirect = true;
807  $logType = 'move_redir';
808  } else {
809  $moveOverRedirect = false;
810  $logType = 'move';
811  }
812 
813  if ( $moveOverRedirect ) {
814  $overwriteMessage = wfMessage(
815  'delete_and_move_reason',
816  $this->oldTitle->getPrefixedText()
817  )->inContentLanguage()->text();
818  $newpage = WikiPage::factory( $nt );
819  $errs = [];
820  $status = $newpage->doDeleteArticleReal(
821  $overwriteMessage,
822  $user,
823  /* $suppress */ false,
824  /* unused */ null,
825  $errs,
826  /* unused */ null,
827  $changeTags,
828  'delete_redir'
829  );
830 
831  if ( !$status->isGood() ) {
832  throw new MWException( 'Failed to delete page-move revision: '
833  . $status->getWikiText( false, false, 'en' ) );
834  }
835 
836  $nt->resetArticleID( false );
837  }
838 
839  if ( $createRedirect ) {
840  if ( $this->oldTitle->getNamespace() === NS_CATEGORY
841  && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
842  ) {
843  $redirectContent = new WikitextContent(
844  wfMessage( 'category-move-redirect-override' )
845  ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
846  } else {
847  $redirectContent = $this->contentHandlerFactory
848  ->getContentHandler( $this->oldTitle->getContentModel() )
849  ->makeRedirectContent(
850  $nt,
851  wfMessage( 'move-redirect-text' )->inContentLanguage()->plain()
852  );
853  }
854 
855  // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
856  } else {
857  $redirectContent = null;
858  }
859 
860  // T59084: log_page should be the ID of the *moved* page
861  $oldid = $this->oldTitle->getArticleID();
862  $logTitle = clone $this->oldTitle;
863 
864  $logEntry = new ManualLogEntry( 'move', $logType );
865  $logEntry->setPerformer( $user );
866  $logEntry->setTarget( $logTitle );
867  $logEntry->setComment( $reason );
868  $logEntry->setParameters( [
869  '4::target' => $nt->getPrefixedText(),
870  '5::noredir' => $redirectContent ? '0' : '1',
871  ] );
872 
873  $formatter = LogFormatter::newFromEntry( $logEntry );
874  $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
875  $comment = $formatter->getPlainActionText();
876  if ( $reason ) {
877  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
878  }
879 
880  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
881 
882  $oldpage = WikiPage::factory( $this->oldTitle );
883  $oldcountable = $oldpage->isCountable();
884 
885  $newpage = WikiPage::factory( $nt );
886 
887  # Change the name of the target page:
888  $dbw->update( 'page',
889  /* SET */ [
890  'page_namespace' => $nt->getNamespace(),
891  'page_title' => $nt->getDBkey(),
892  ],
893  /* WHERE */ [ 'page_id' => $oldid ],
894  __METHOD__
895  );
896 
897  // Reset $nt before using it to create the null revision (T248789).
898  // But not $this->oldTitle yet, see below (T47348).
899  $nt->resetArticleID( $oldid );
900 
901  $commentObj = CommentStoreComment::newUnsavedComment( $comment );
902  # Save a null revision in the page's history notifying of the move
903  $nullRevision = $this->revisionStore->newNullRevision(
904  $dbw,
905  $nt,
906  $commentObj,
907  true,
908  $user
909  );
910  if ( $nullRevision === null ) {
911  $id = $nt->getArticleID( Title::READ_EXCLUSIVE );
912  $msg = 'Failed to create null revision while moving page ID ' .
913  $oldid . ' to ' . $nt->getPrefixedDBkey() . " (page ID $id)";
914 
915  throw new MWException( $msg );
916  }
917 
918  $nullRevision = $this->revisionStore->insertRevisionOn( $nullRevision, $dbw );
919  $logEntry->setAssociatedRevId( $nullRevision->getId() );
920 
926  $user->incEditCount();
927 
928  if ( !$redirectContent ) {
929  // Clean up the old title *before* reset article id - T47348
930  WikiPage::onArticleDelete( $this->oldTitle );
931  }
932 
933  $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
934  $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
935 
936  $newpage->updateRevisionOn( $dbw, $nullRevision );
937 
938  $fakeTags = [];
939  $this->hookRunner->onRevisionFromEditComplete(
940  $newpage, $nullRevision, $nullRevision->getParentId(), $user, $fakeTags );
941 
942  // Hook is hard deprecated since 1.35
943  if ( $this->hookContainer->isRegistered( 'NewRevisionFromEditComplete' ) ) {
944  // Only create the Revision object if needed
945  $nullRevisionObj = new Revision( $nullRevision );
946  $this->hookRunner->onNewRevisionFromEditComplete(
947  $newpage,
948  $nullRevisionObj,
949  $nullRevision->getParentId(),
950  $user,
951  $fakeTags
952  );
953  }
954 
955  $newpage->doEditUpdates( $nullRevision, $user,
956  [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
957 
959 
960  # Recreate the redirect, this time in the other direction.
961  if ( $redirectContent ) {
962  $redirectArticle = WikiPage::factory( $this->oldTitle );
963  $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
964  $newid = $redirectArticle->insertOn( $dbw );
965  if ( $newid ) { // sanity
966  $this->oldTitle->resetArticleID( $newid );
967  $redirectRevisionRecord = new MutableRevisionRecord( $this->oldTitle );
968  $redirectRevisionRecord->setPageId( $newid );
969  $redirectRevisionRecord->setUser( $user );
970  $redirectRevisionRecord->setComment( $commentObj );
971  $redirectRevisionRecord->setContent( SlotRecord::MAIN, $redirectContent );
972  $redirectRevisionRecord->setTimestamp( MWTimestamp::now( TS_MW ) );
973 
974  $inserted = $this->revisionStore->insertRevisionOn(
975  $redirectRevisionRecord,
976  $dbw
977  );
978  $redirectRevId = $inserted->getId();
979  $redirectArticle->updateRevisionOn( $dbw, $inserted, 0 );
980 
981  $fakeTags = [];
982  $this->hookRunner->onRevisionFromEditComplete(
983  $redirectArticle,
984  $inserted,
985  false,
986  $user,
987  $fakeTags
988  );
989 
990  // Hook is hard deprecated since 1.35
991  if ( $this->hookContainer->isRegistered( 'NewRevisionFromEditComplete' ) ) {
992  // Only create the Revision object if needed
993  $redirectRevisionObj = new Revision( $inserted );
994  $this->hookRunner->onNewRevisionFromEditComplete(
995  $redirectArticle,
996  $redirectRevisionObj,
997  false,
998  $user,
999  $fakeTags
1000  );
1001  }
1002 
1003  $redirectArticle->doEditUpdates(
1004  $inserted,
1005  $user,
1006  [ 'created' => true ]
1007  );
1008 
1009  // make a copy because of log entry below
1010  $redirectTags = $changeTags;
1011  if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
1012  $redirectTags[] = 'mw-new-redirect';
1013  }
1014  ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
1015  }
1016  }
1017 
1018  # Log the move
1019  $logid = $logEntry->insert();
1020 
1021  $logEntry->addTags( $changeTags );
1022  $logEntry->publish( $logid );
1023 
1024  return $nullRevision;
1025  }
1026 }
MovePage\__construct
__construct(Title $oldTitle, Title $newTitle, ServiceOptions $options=null, ILoadBalancer $loadBalancer=null, NamespaceInfo $nsInfo=null, WatchedItemStoreInterface $watchedItems=null, PermissionManager $permMgr=null, RepoGroup $repoGroup=null, IContentHandlerFactory $contentHandlerFactory=null, RevisionStore $revisionStore=null, SpamChecker $spamChecker=null, HookContainer $hookContainer=null)
Calling this directly is deprecated in 1.34.
Definition: MovePage.php:132
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
Definition: GlobalFunctions.php:180
MovePage\checkPermissions
checkPermissions(User $user, $reason)
Check if the user is allowed to perform the move.
Definition: MovePage.php:177
File\checkExtensionCompatibility
static checkExtensionCompatibility(File $old, $new)
Checks if file extensions are compatible.
Definition: File.php:264
WikiPage\onArticleCreate
static onArticleCreate(Title $title)
The onArticle*() functions are supposed to be a kind of hooks which should be called whenever any of ...
Definition: WikiPage.php:3464
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
MovePage\$loadBalancer
ILoadBalancer $loadBalancer
Definition: MovePage.php:62
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
$wgMaximumMovedPages
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
Definition: DefaultSettings.php:9020
MovePage\isValidMoveTarget
isValidMoveTarget()
Checks if $this can be moved to a given Title.
Definition: MovePage.php:325
MovePage\move
move(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move a page without taking user permissions into account.
Definition: MovePage.php:381
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:158
MovePage\$oldTitle
Title $oldTitle
Definition: MovePage.php:47
User\incEditCount
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition: User.php:4341
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:81
MovePage\$revisionStore
RevisionStore $revisionStore
Definition: MovePage.php:92
User\spreadAnyEditBlock
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
Definition: User.php:3645
MovePage\moveFile
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
Definition: MovePage.php:772
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:106
MovePage\isValidMove
isValidMove()
Does various sanity checks that the move is valid.
Definition: MovePage.php:217
StringUtils\escapeRegexReplacement
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Definition: StringUtils.php:314
NS_FILE
const NS_FILE
Definition: Defines.php:75
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
RequestContext\newExtraneousContext
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
Definition: RequestContext.php:621
$res
$res
Definition: testCompression.php:57
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
Collation\singleton
static singleton()
Definition: Collation.php:37
Revision
Definition: Revision.php:40
MovePage\$options
ServiceOptions $options
Definition: MovePage.php:57
MWException
MediaWiki exception.
Definition: MWException.php:29
wfStripIllegalFilenameChars
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
Definition: GlobalFunctions.php:2606
MovePage\$repoGroup
RepoGroup $repoGroup
Definition: MovePage.php:82
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:157
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
MovePage
Handles the backend logic of moving a page from one title to another.
Definition: MovePage.php:42
MovePage\moveSubpagesIfAllowed
moveSubpagesIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, with user permission checks.
Definition: MovePage.php:457
MovePage\$nsInfo
NamespaceInfo $nsInfo
Definition: MovePage.php:67
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3500
MovePage\$hookRunner
HookRunner $hookRunner
Definition: MovePage.php:107
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:78
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
MovePage\moveSubpages
moveSubpages(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, without checking user permissions.
Definition: MovePage.php:438
DB_MASTER
const DB_MASTER
Definition: defines.php:26
WikitextContent
Content object for wiki text pages.
Definition: WikitextContent.php:37
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:910
AtomicSectionUpdate
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
Definition: AtomicSectionUpdate.php:9
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:49
$content
$content
Definition: router.php:76
MovePage\moveIfAllowed
moveIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Same as move(), but with permissions checks.
Definition: MovePage.php:401
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:299
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:45
MediaWiki\EditPage\SpamChecker
Service to check if text (either content or a summary) qualifies as spam.
Definition: SpamChecker.php:14
MovePage\moveToInternal
moveToInternal(User $user, &$nt, $reason='', $createRedirect=true, array $changeTags=[])
Move page to a title which is either a redirect to the source page or nonexistent.
Definition: MovePage.php:802
MovePage\$newTitle
Title $newTitle
Definition: MovePage.php:52
MovePage\$watchedItems
WatchedItemStoreInterface $watchedItems
Definition: MovePage.php:72
MovePage\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: MovePage.php:87
MovePage\moveUnsafe
moveUnsafe(User $user, $reason, $createRedirect, array $changeTags)
Moves without any sort of safety or sanity checks.
Definition: MovePage.php:557
ChangeTags\canAddTagsAccompanyingChange
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:545
Title
Represents a title within MediaWiki.
Definition: Title.php:41
RepoGroup
Prioritized list of file repositories.
Definition: RepoGroup.php:31
MovePage\isValidFileMove
isValidFileMove()
Sanity checks for when a file is being moved.
Definition: MovePage.php:295
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:42
MovePage\$permMgr
PermissionManager $permMgr
Definition: MovePage.php:77
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
MovePage\$spamChecker
SpamChecker $spamChecker
Definition: MovePage.php:97
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:564
MovePage\$hookContainer
HookContainer $hookContainer
Definition: MovePage.php:102
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:30
MovePage\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: MovePage.php:112
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
MovePage\moveSubpagesInternal
moveSubpagesInternal( $checkPermissions, User $user, $reason, $createRedirect, array $changeTags)
Definition: MovePage.php:471
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
ChangeTags\addTags
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:273
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
LogFormatter\newFromEntry
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
Definition: LogFormatter.php:52
$type
$type
Definition: testCompression.php:52