MediaWiki  1.34.0
MovePage.php
Go to the documentation of this file.
1 <?php
2 
29 
36 class MovePage {
37 
41  protected $oldTitle;
42 
46  protected $newTitle;
47 
51  protected $options;
52 
56  protected $loadBalancer;
57 
61  protected $nsInfo;
62 
66  protected $watchedItems;
67 
71  protected $permMgr;
72 
76  protected $repoGroup;
77 
89  public function __construct(
92  ServiceOptions $options = null,
94  NamespaceInfo $nsInfo = null,
97  RepoGroup $repoGroup = null
98  ) {
99  $this->oldTitle = $oldTitle;
100  $this->newTitle = $newTitle;
101  $this->options = $options ??
102  new ServiceOptions( MovePageFactory::$constructorOptions,
103  MediaWikiServices::getInstance()->getMainConfig() );
104  $this->loadBalancer =
105  $loadBalancer ?? MediaWikiServices::getInstance()->getDBLoadBalancer();
106  $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
107  $this->watchedItems =
108  $watchedItems ?? MediaWikiServices::getInstance()->getWatchedItemStore();
109  $this->permMgr = $permMgr ?? MediaWikiServices::getInstance()->getPermissionManager();
110  $this->repoGroup = $repoGroup ?? MediaWikiServices::getInstance()->getRepoGroup();
111  }
112 
121  public function checkPermissions( User $user, $reason ) {
122  $status = new Status();
123 
124  $errors = wfMergeErrorArrays(
125  $this->permMgr->getPermissionErrors( 'move', $user, $this->oldTitle ),
126  $this->permMgr->getPermissionErrors( 'edit', $user, $this->oldTitle ),
127  $this->permMgr->getPermissionErrors( 'move-target', $user, $this->newTitle ),
128  $this->permMgr->getPermissionErrors( 'edit', $user, $this->newTitle )
129  );
130 
131  // Convert into a Status object
132  if ( $errors ) {
133  foreach ( $errors as $error ) {
134  $status->fatal( ...$error );
135  }
136  }
137 
138  if ( $reason !== null && EditPage::matchSummarySpamRegex( $reason ) !== false ) {
139  // This is kind of lame, won't display nice
140  $status->fatal( 'spamprotectiontext' );
141  }
142 
143  $tp = $this->newTitle->getTitleProtection();
144  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
145  if ( $tp !== false && !$permissionManager->userHasRight( $user, $tp['permission'] ) ) {
146  $status->fatal( 'cantmove-titleprotected' );
147  }
148 
149  Hooks::run( 'MovePageCheckPermissions',
150  [ $this->oldTitle, $this->newTitle, $user, $reason, $status ]
151  );
152 
153  return $status;
154  }
155 
163  public function isValidMove() {
164  $status = new Status();
165 
166  if ( $this->oldTitle->equals( $this->newTitle ) ) {
167  $status->fatal( 'selfmove' );
168  } elseif ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) {
169  // The move is allowed only if (1) the target doesn't exist, or (2) the target is a
170  // redirect to the source, and has no history (so we can undo bad moves right after
171  // they're done).
172  $status->fatal( 'articleexists' );
173  }
174 
175  // @todo If the old title is invalid, maybe we should check if it somehow exists in the
176  // database and allow moving it to a valid name? Why prohibit the move from an empty name
177  // without checking in the database?
178  if ( $this->oldTitle->getDBkey() == '' ) {
179  $status->fatal( 'badarticleerror' );
180  } elseif ( $this->oldTitle->isExternal() ) {
181  $status->fatal( 'immobile-source-namespace-iw' );
182  } elseif ( !$this->oldTitle->isMovable() ) {
183  $status->fatal( 'immobile-source-namespace', $this->oldTitle->getNsText() );
184  } elseif ( !$this->oldTitle->exists() ) {
185  $status->fatal( 'movepage-source-doesnt-exist' );
186  }
187 
188  if ( $this->newTitle->isExternal() ) {
189  $status->fatal( 'immobile-target-namespace-iw' );
190  } elseif ( !$this->newTitle->isMovable() ) {
191  $status->fatal( 'immobile-target-namespace', $this->newTitle->getNsText() );
192  }
193  if ( !$this->newTitle->isValid() ) {
194  $status->fatal( 'movepage-invalid-target-title' );
195  }
196 
197  // Content model checks
198  if ( !$this->options->get( 'ContentHandlerUseDB' ) &&
199  $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) {
200  // can't move a page if that would change the page's content model
201  $status->fatal(
202  'bad-target-model',
203  ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
204  ContentHandler::getLocalizedName( $this->newTitle->getContentModel() )
205  );
206  } elseif (
207  !ContentHandler::getForTitle( $this->oldTitle )->canBeUsedOn( $this->newTitle )
208  ) {
209  $status->fatal(
210  'content-not-allowed-here',
211  ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
212  $this->newTitle->getPrefixedText(),
213  SlotRecord::MAIN
214  );
215  }
216 
217  // Image-specific checks
218  if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
219  $status->merge( $this->isValidFileMove() );
220  }
221 
222  if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
223  $status->fatal( 'nonfile-cannot-move-to-file' );
224  }
225 
226  // Hook for extensions to say a title can't be moved for technical reasons
227  Hooks::run( 'MovePageIsValidMove', [ $this->oldTitle, $this->newTitle, $status ] );
228 
229  return $status;
230  }
231 
237  protected function isValidFileMove() {
238  $status = new Status();
239 
240  if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
241  $status->fatal( 'imagenocrossnamespace' );
242  // No need for further errors about the target filename being wrong
243  return $status;
244  }
245 
246  $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
247  $file->load( File::READ_LATEST );
248  if ( $file->exists() ) {
249  if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) {
250  $status->fatal( 'imageinvalidfilename' );
251  }
252  if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
253  $status->fatal( 'imagetypemismatch' );
254  }
255  }
256 
257  return $status;
258  }
259 
267  protected function isValidMoveTarget() {
268  # Is it an existing file?
269  if ( $this->newTitle->inNamespace( NS_FILE ) ) {
270  $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
271  $file->load( File::READ_LATEST );
272  if ( $file->exists() ) {
273  wfDebug( __METHOD__ . ": file exists\n" );
274  return false;
275  }
276  }
277  # Is it a redirect with no history?
278  if ( !$this->newTitle->isSingleRevRedirect() ) {
279  wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
280  return false;
281  }
282  # Get the article text
283  $rev = Revision::newFromTitle( $this->newTitle, false, Revision::READ_LATEST );
284  if ( !is_object( $rev ) ) {
285  return false;
286  }
287  $content = $rev->getContent();
288  # Does the redirect point to the source?
289  # Or is it a broken self-redirect, usually caused by namespace collisions?
290  $redirTitle = $content ? $content->getRedirectTarget() : null;
291 
292  if ( $redirTitle ) {
293  if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
294  $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
295  wfDebug( __METHOD__ . ": redirect points to other page\n" );
296  return false;
297  } else {
298  return true;
299  }
300  } else {
301  # Fail safe (not a redirect after all. strange.)
302  wfDebug( __METHOD__ . ": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
303  " is a redirect, but it doesn't contain a valid redirect.\n" );
304  return false;
305  }
306  }
307 
319  public function move(
320  User $user, $reason = null, $createRedirect = true, array $changeTags = []
321  ) {
322  $status = $this->isValidMove();
323  if ( !$status->isOK() ) {
324  return $status;
325  }
326 
327  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
328  }
329 
339  public function moveIfAllowed(
340  User $user, $reason = null, $createRedirect = true, array $changeTags = []
341  ) {
342  $status = $this->isValidMove();
343  $status->merge( $this->checkPermissions( $user, $reason ) );
344  if ( $changeTags ) {
345  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $user ) );
346  }
347 
348  if ( !$status->isOK() ) {
349  // Auto-block user's IP if the account was "hard" blocked
350  $user->spreadAnyEditBlock();
351  return $status;
352  }
353 
354  // Check suppressredirect permission
355  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
356  if ( !$permissionManager->userHasRight( $user, 'suppressredirect' ) ) {
357  $createRedirect = true;
358  }
359 
360  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
361  }
362 
377  public function moveSubpages(
378  User $user, $reason = null, $createRedirect = true, array $changeTags = []
379  ) {
380  return $this->moveSubpagesInternal( false, $user, $reason, $createRedirect, $changeTags );
381  }
382 
396  public function moveSubpagesIfAllowed(
397  User $user, $reason = null, $createRedirect = true, array $changeTags = []
398  ) {
399  return $this->moveSubpagesInternal( true, $user, $reason, $createRedirect, $changeTags );
400  }
401 
410  private function moveSubpagesInternal(
411  $checkPermissions, User $user, $reason, $createRedirect, array $changeTags
412  ) {
413  global $wgMaximumMovedPages;
414  $services = MediaWikiServices::getInstance();
415 
416  if ( $checkPermissions ) {
417  if ( !$services->getPermissionManager()->userCan(
418  'move-subpages', $user, $this->oldTitle )
419  ) {
420  return Status::newFatal( 'cant-move-subpages' );
421  }
422  }
423 
424  $nsInfo = $services->getNamespaceInfo();
425 
426  // Do the source and target namespaces support subpages?
427  if ( !$nsInfo->hasSubpages( $this->oldTitle->getNamespace() ) ) {
428  return Status::newFatal( 'namespace-nosubpages',
429  $nsInfo->getCanonicalName( $this->oldTitle->getNamespace() ) );
430  }
431  if ( !$nsInfo->hasSubpages( $this->newTitle->getNamespace() ) ) {
432  return Status::newFatal( 'namespace-nosubpages',
433  $nsInfo->getCanonicalName( $this->newTitle->getNamespace() ) );
434  }
435 
436  // Return a status for the overall result. Its value will be an array with per-title
437  // status for each subpage. Merge any errors from the per-title statuses into the
438  // top-level status without resetting the overall result.
439  $topStatus = Status::newGood();
440  $perTitleStatus = [];
441  $subpages = $this->oldTitle->getSubpages( $wgMaximumMovedPages + 1 );
442  $count = 0;
443  foreach ( $subpages as $oldSubpage ) {
444  $count++;
445  if ( $count > $wgMaximumMovedPages ) {
446  $status = Status::newFatal( 'movepage-max-pages', $wgMaximumMovedPages );
447  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
448  $topStatus->merge( $status );
449  $topStatus->setOK( true );
450  break;
451  }
452 
453  // We don't know whether this function was called before or after moving the root page,
454  // so check both titles
455  if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
456  $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
457  ) {
458  // When moving a page to a subpage of itself, don't move it twice
459  continue;
460  }
461  $newPageName = preg_replace(
462  '#^' . preg_quote( $this->oldTitle->getDBkey(), '#' ) . '#',
463  StringUtils::escapeRegexReplacement( $this->newTitle->getDBkey() ), # T23234
464  $oldSubpage->getDBkey() );
465  if ( $oldSubpage->isTalkPage() ) {
466  $newNs = $this->newTitle->getTalkPage()->getNamespace();
467  } else {
468  $newNs = $this->newTitle->getSubjectPage()->getNamespace();
469  }
470  // T16385: we need makeTitleSafe because the new page names may be longer than 255
471  // characters.
472  $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
473 
474  $mp = new MovePage( $oldSubpage, $newSubpage );
475  $method = $checkPermissions ? 'moveIfAllowed' : 'move';
477  $status = $mp->$method( $user, $reason, $createRedirect, $changeTags );
478  if ( $status->isOK() ) {
479  $status->setResult( true, $newSubpage->getPrefixedText() );
480  }
481  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
482  $topStatus->merge( $status );
483  $topStatus->setOK( true );
484  }
485 
486  $topStatus->value = $perTitleStatus;
487  return $topStatus;
488  }
489 
499  private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) {
501  Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user, $reason, &$status ] );
502  if ( !$status->isOK() ) {
503  // Move was aborted by the hook
504  return $status;
505  }
506 
507  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
508  $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
509 
510  Hooks::run( 'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] );
511 
512  $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
513  $protected = $this->oldTitle->isProtected();
514 
515  // Do the actual move; if this fails, it will throw an MWException(!)
516  $nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
517  $changeTags );
518 
519  // Refresh the sortkey for this row. Be careful to avoid resetting
520  // cl_timestamp, which may disturb time-based lists on some sites.
521  // @todo This block should be killed, it's duplicating code
522  // from LinksUpdate::getCategoryInsertions() and friends.
523  $prefixes = $dbw->select(
524  'categorylinks',
525  [ 'cl_sortkey_prefix', 'cl_to' ],
526  [ 'cl_from' => $pageid ],
527  __METHOD__
528  );
529  $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() );
530  foreach ( $prefixes as $prefixRow ) {
531  $prefix = $prefixRow->cl_sortkey_prefix;
532  $catTo = $prefixRow->cl_to;
533  $dbw->update( 'categorylinks',
534  [
535  'cl_sortkey' => Collation::singleton()->getSortKey(
536  $this->newTitle->getCategorySortkey( $prefix ) ),
537  'cl_collation' => $this->options->get( 'CategoryCollation' ),
538  'cl_type' => $type,
539  'cl_timestamp=cl_timestamp' ],
540  [
541  'cl_from' => $pageid,
542  'cl_to' => $catTo ],
543  __METHOD__
544  );
545  }
546 
547  $redirid = $this->oldTitle->getArticleID();
548 
549  if ( $protected ) {
550  # Protect the redirect title as the title used to be...
551  $res = $dbw->select(
552  'page_restrictions',
553  [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
554  [ 'pr_page' => $pageid ],
555  __METHOD__,
556  'FOR UPDATE'
557  );
558  $rowsInsert = [];
559  foreach ( $res as $row ) {
560  $rowsInsert[] = [
561  'pr_page' => $redirid,
562  'pr_type' => $row->pr_type,
563  'pr_level' => $row->pr_level,
564  'pr_cascade' => $row->pr_cascade,
565  'pr_user' => $row->pr_user,
566  'pr_expiry' => $row->pr_expiry
567  ];
568  }
569  $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
570 
571  // Build comment for log
572  $comment = wfMessage(
573  'prot_1movedto2',
574  $this->oldTitle->getPrefixedText(),
575  $this->newTitle->getPrefixedText()
576  )->inContentLanguage()->text();
577  if ( $reason ) {
578  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
579  }
580 
581  // reread inserted pr_ids for log relation
582  $insertedPrIds = $dbw->select(
583  'page_restrictions',
584  'pr_id',
585  [ 'pr_page' => $redirid ],
586  __METHOD__
587  );
588  $logRelationsValues = [];
589  foreach ( $insertedPrIds as $prid ) {
590  $logRelationsValues[] = $prid->pr_id;
591  }
592 
593  // Update the protection log
594  $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
595  $logEntry->setTarget( $this->newTitle );
596  $logEntry->setComment( $comment );
597  $logEntry->setPerformer( $user );
598  $logEntry->setParameters( [
599  '4::oldtitle' => $this->oldTitle->getPrefixedText(),
600  ] );
601  $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
602  $logEntry->addTags( $changeTags );
603  $logId = $logEntry->insert();
604  $logEntry->publish( $logId );
605  }
606 
607  // Update *_from_namespace fields as needed
608  if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
609  $dbw->update( 'pagelinks',
610  [ 'pl_from_namespace' => $this->newTitle->getNamespace() ],
611  [ 'pl_from' => $pageid ],
612  __METHOD__
613  );
614  $dbw->update( 'templatelinks',
615  [ 'tl_from_namespace' => $this->newTitle->getNamespace() ],
616  [ 'tl_from' => $pageid ],
617  __METHOD__
618  );
619  $dbw->update( 'imagelinks',
620  [ 'il_from_namespace' => $this->newTitle->getNamespace() ],
621  [ 'il_from' => $pageid ],
622  __METHOD__
623  );
624  }
625 
626  # Update watchlists
627  $oldtitle = $this->oldTitle->getDBkey();
628  $newtitle = $this->newTitle->getDBkey();
629  $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
630  $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
631  if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
632  $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
633  }
634 
635  // If it is a file then move it last.
636  // This is done after all database changes so that file system errors cancel the transaction.
637  if ( $this->oldTitle->getNamespace() == NS_FILE ) {
638  $status = $this->moveFile( $this->oldTitle, $this->newTitle );
639  if ( !$status->isOK() ) {
640  $dbw->cancelAtomic( __METHOD__ );
641  return $status;
642  }
643  }
644 
645  Hooks::run(
646  'TitleMoveCompleting',
647  [ $this->oldTitle, $this->newTitle,
648  $user, $pageid, $redirid, $reason, $nullRevision ]
649  );
650 
651  $dbw->endAtomic( __METHOD__ );
652 
653  $params = [
656  &$user,
657  $pageid,
658  $redirid,
659  $reason,
660  $nullRevision
661  ];
662  // Keep each single hook handler atomic
665  $dbw,
666  __METHOD__,
667  // Hold onto $user to avoid HHVM bug where it no longer
668  // becomes a reference (T118683)
669  function () use ( $params, &$user ) {
670  Hooks::run( 'TitleMoveComplete', $params );
671  }
672  )
673  );
674 
675  return Status::newGood();
676  }
677 
687  private function moveFile( $oldTitle, $newTitle ) {
689  'cannotdelete',
691  );
692 
693  $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle );
694  $file->load( File::READ_LATEST );
695  if ( $file->exists() ) {
696  $status = $file->move( $newTitle );
697  }
698 
699  // Clear RepoGroup process cache
700  $this->repoGroup->clearCache( $oldTitle );
701  $this->repoGroup->clearCache( $newTitle ); # clear false negative cache
702  return $status;
703  }
704 
720  private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true,
721  array $changeTags = []
722  ) {
723  if ( $nt->exists() ) {
724  $moveOverRedirect = true;
725  $logType = 'move_redir';
726  } else {
727  $moveOverRedirect = false;
728  $logType = 'move';
729  }
730 
731  if ( $moveOverRedirect ) {
732  $overwriteMessage = wfMessage(
733  'delete_and_move_reason',
734  $this->oldTitle->getPrefixedText()
735  )->inContentLanguage()->text();
736  $newpage = WikiPage::factory( $nt );
737  $errs = [];
738  $status = $newpage->doDeleteArticleReal(
739  $overwriteMessage,
740  /* $suppress */ false,
741  $nt->getArticleID(),
742  /* $commit */ false,
743  $errs,
744  $user,
745  $changeTags,
746  'delete_redir'
747  );
748 
749  if ( !$status->isGood() ) {
750  throw new MWException( 'Failed to delete page-move revision: '
751  . $status->getWikiText( false, false, 'en' ) );
752  }
753 
754  $nt->resetArticleID( false );
755  }
756 
757  if ( $createRedirect ) {
758  if ( $this->oldTitle->getNamespace() == NS_CATEGORY
759  && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
760  ) {
761  $redirectContent = new WikitextContent(
762  wfMessage( 'category-move-redirect-override' )
763  ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
764  } else {
765  $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
766  $redirectContent = $contentHandler->makeRedirectContent( $nt,
767  wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
768  }
769 
770  // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
771  } else {
772  $redirectContent = null;
773  }
774 
775  // Figure out whether the content model is no longer the default
776  $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle );
777  $contentModel = $this->oldTitle->getContentModel();
778  $newDefault = ContentHandler::getDefaultModelFor( $nt );
779  $defaultContentModelChanging = ( $oldDefault !== $newDefault
780  && $oldDefault === $contentModel );
781 
782  // T59084: log_page should be the ID of the *moved* page
783  $oldid = $this->oldTitle->getArticleID();
784  $logTitle = clone $this->oldTitle;
785 
786  $logEntry = new ManualLogEntry( 'move', $logType );
787  $logEntry->setPerformer( $user );
788  $logEntry->setTarget( $logTitle );
789  $logEntry->setComment( $reason );
790  $logEntry->setParameters( [
791  '4::target' => $nt->getPrefixedText(),
792  '5::noredir' => $redirectContent ? '0' : '1',
793  ] );
794 
795  $formatter = LogFormatter::newFromEntry( $logEntry );
796  $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
797  $comment = $formatter->getPlainActionText();
798  if ( $reason ) {
799  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
800  }
801 
802  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
803 
804  $oldpage = WikiPage::factory( $this->oldTitle );
805  $oldcountable = $oldpage->isCountable();
806 
807  $newpage = WikiPage::factory( $nt );
808 
809  # Change the name of the target page:
810  $dbw->update( 'page',
811  /* SET */ [
812  'page_namespace' => $nt->getNamespace(),
813  'page_title' => $nt->getDBkey(),
814  ],
815  /* WHERE */ [ 'page_id' => $oldid ],
816  __METHOD__
817  );
818 
819  # Save a null revision in the page's history notifying of the move
820  $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
821  if ( !is_object( $nullRevision ) ) {
822  throw new MWException( 'Failed to create null revision while moving page ID '
823  . $oldid . ' to ' . $nt->getPrefixedDBkey() );
824  }
825 
826  $nullRevId = $nullRevision->insertOn( $dbw );
827  $logEntry->setAssociatedRevId( $nullRevId );
828 
834  $user->incEditCount();
835 
836  if ( !$redirectContent ) {
837  // Clean up the old title *before* reset article id - T47348
838  WikiPage::onArticleDelete( $this->oldTitle );
839  }
840 
841  $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
842  $nt->resetArticleID( $oldid );
843  $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
844 
845  $newpage->updateRevisionOn( $dbw, $nullRevision );
846 
847  Hooks::run( 'NewRevisionFromEditComplete',
848  [ $newpage, $nullRevision, $nullRevision->getParentId(), $user ] );
849 
850  $newpage->doEditUpdates( $nullRevision, $user,
851  [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
852 
853  // If the default content model changes, we need to populate rev_content_model
854  if ( $defaultContentModelChanging ) {
855  $dbw->update(
856  'revision',
857  [ 'rev_content_model' => $contentModel ],
858  [ 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ],
859  __METHOD__
860  );
861  }
862 
864 
865  # Recreate the redirect, this time in the other direction.
866  if ( $redirectContent ) {
867  $redirectArticle = WikiPage::factory( $this->oldTitle );
868  $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
869  $newid = $redirectArticle->insertOn( $dbw );
870  if ( $newid ) { // sanity
871  $this->oldTitle->resetArticleID( $newid );
872  $redirectRevision = new Revision( [
873  'title' => $this->oldTitle, // for determining the default content model
874  'page' => $newid,
875  'user_text' => $user->getName(),
876  'user' => $user->getId(),
877  'comment' => $comment,
878  'content' => $redirectContent ] );
879  $redirectRevId = $redirectRevision->insertOn( $dbw );
880  $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
881 
882  Hooks::run( 'NewRevisionFromEditComplete',
883  [ $redirectArticle, $redirectRevision, false, $user ] );
884 
885  $redirectArticle->doEditUpdates( $redirectRevision, $user, [ 'created' => true ] );
886 
887  // make a copy because of log entry below
888  $redirectTags = $changeTags;
889  if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
890  $redirectTags[] = 'mw-new-redirect';
891  }
892  ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
893  }
894  }
895 
896  # Log the move
897  $logid = $logEntry->insert();
898 
899  $logEntry->addTags( $changeTags );
900  $logEntry->publish( $logid );
901 
902  return $nullRevision;
903  }
904 }
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
Definition: GlobalFunctions.php:181
MovePage\checkPermissions
checkPermissions(User $user, $reason)
Check if the user is allowed to perform the move.
Definition: MovePage.php:121
File\checkExtensionCompatibility
static checkExtensionCompatibility(File $old, $new)
Checks if file extensions are compatible.
Definition: File.php:259
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:3392
MovePage\$loadBalancer
ILoadBalancer $loadBalancer
Definition: MovePage.php:56
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
User\getId
getId()
Get the user's ID.
Definition: User.php:2203
$wgMaximumMovedPages
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
Definition: DefaultSettings.php:8550
MovePage\isValidMoveTarget
isValidMoveTarget()
Checks if $this can be moved to a given Title.
Definition: MovePage.php:267
MovePage\move
move(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move a page without taking user permissions into account.
Definition: MovePage.php:319
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
MovePage\$oldTitle
Title $oldTitle
Definition: MovePage.php:41
User\incEditCount
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition: User.php:4959
User\spreadAnyEditBlock
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
Definition: User.php:4190
MovePage\moveFile
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
Definition: MovePage.php:687
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1818
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Definition: DeferredUpdates.php:85
MovePage\isValidMove
isValidMove()
Does various sanity checks that the move is valid.
Definition: MovePage.php:163
StringUtils\escapeRegexReplacement
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Definition: StringUtils.php:343
NS_FILE
const NS_FILE
Definition: Defines.php:66
$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:1264
RequestContext\newExtraneousContext
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
Definition: RequestContext.php:601
ContentHandler\getForTitle
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
Definition: ContentHandler.php:201
$res
$res
Definition: testCompression.php:52
Revision\insertOn
insertOn( $dbw)
Insert a new revision into the database, returning the new revision ID number on success and dies hor...
Definition: Revision.php:954
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
NamespaceInfo\hasSubpages
hasSubpages( $index)
Does the namespace allow subpages?
Definition: NamespaceInfo.php:464
IDBAccessObject\READ_LOCKING
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Definition: IDBAccessObject.php:64
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:40
Collation\singleton
static singleton()
Definition: Collation.php:36
Revision
Definition: Revision.php:40
Revision\newFromTitle
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: Revision.php:138
MovePage\$options
ServiceOptions $options
Definition: MovePage.php:51
ContentHandler\getDefaultModelFor
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title.
Definition: ContentHandler.php:186
MWException
MediaWiki exception.
Definition: MWException.php:26
wfStripIllegalFilenameChars
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
Definition: GlobalFunctions.php:2753
MovePage\$repoGroup
RepoGroup $repoGroup
Definition: MovePage.php:76
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
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:36
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:396
MovePage\$nsInfo
NamespaceInfo $nsInfo
Definition: MovePage.php:61
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3428
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:58
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:74
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:377
DB_MASTER
const DB_MASTER
Definition: defines.php:26
WikitextContent
Content object for wiki text pages.
Definition: WikitextContent.php:36
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:913
MediaWiki\Page\MovePageFactory
Definition: MovePageFactory.php:36
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:613
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:47
$content
$content
Definition: router.php:78
MovePage\moveIfAllowed
moveIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Same as move(), but with permissions checks.
Definition: MovePage.php:339
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:318
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
EditPage\matchSummarySpamRegex
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match.
Definition: EditPage.php:2509
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:720
MovePage\$newTitle
Title $newTitle
Definition: MovePage.php:46
MovePage\$watchedItems
WatchedItemStoreInterface $watchedItems
Definition: MovePage.php:66
MovePage\moveUnsafe
moveUnsafe(User $user, $reason, $createRedirect, array $changeTags)
Moves without any sort of safety or sanity checks.
Definition: MovePage.php:499
NamespaceInfo\getCanonicalName
getCanonicalName( $index)
Returns the canonical (English) name for a given index.
Definition: NamespaceInfo.php:367
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:521
Title
Represents a title within MediaWiki.
Definition: Title.php:42
$status
return $status
Definition: SyntaxHighlight.php:347
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)
Calling this directly is deprecated in 1.34.
Definition: MovePage.php:89
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:237
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:37
MovePage\$permMgr
PermissionManager $permMgr
Definition: MovePage.php:71
Revision\newNullRevision
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1000
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:33
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:30
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2232
MovePage\moveSubpagesInternal
moveSubpagesInternal( $checkPermissions, User $user, $reason, $createRedirect, array $changeTags)
Definition: MovePage.php:410
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:251
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:50
$type
$type
Definition: testCompression.php:48