MediaWiki  master
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::CONSTRUCTOR_OPTIONS,
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 ) {
500  $status = Status::newGood();
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  function () use ( $params ) {
668  Hooks::run( 'TitleMoveComplete', $params );
669  }
670  )
671  );
672 
673  return Status::newGood();
674  }
675 
685  private function moveFile( $oldTitle, $newTitle ) {
686  $status = Status::newFatal(
687  'cannotdelete',
689  );
690 
691  $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle );
692  $file->load( File::READ_LATEST );
693  if ( $file->exists() ) {
694  $status = $file->move( $newTitle );
695  }
696 
697  // Clear RepoGroup process cache
698  $this->repoGroup->clearCache( $oldTitle );
699  $this->repoGroup->clearCache( $newTitle ); # clear false negative cache
700  return $status;
701  }
702 
718  private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true,
719  array $changeTags = []
720  ) {
721  if ( $nt->exists() ) {
722  $moveOverRedirect = true;
723  $logType = 'move_redir';
724  } else {
725  $moveOverRedirect = false;
726  $logType = 'move';
727  }
728 
729  if ( $moveOverRedirect ) {
730  $overwriteMessage = wfMessage(
731  'delete_and_move_reason',
732  $this->oldTitle->getPrefixedText()
733  )->inContentLanguage()->text();
734  $newpage = WikiPage::factory( $nt );
735  $errs = [];
736  $status = $newpage->doDeleteArticleReal(
737  $overwriteMessage,
738  /* $suppress */ false,
739  $nt->getArticleID(),
740  /* $commit */ false,
741  $errs,
742  $user,
743  $changeTags,
744  'delete_redir'
745  );
746 
747  if ( !$status->isGood() ) {
748  throw new MWException( 'Failed to delete page-move revision: '
749  . $status->getWikiText( false, false, 'en' ) );
750  }
751 
752  $nt->resetArticleID( false );
753  }
754 
755  if ( $createRedirect ) {
756  if ( $this->oldTitle->getNamespace() == NS_CATEGORY
757  && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
758  ) {
759  $redirectContent = new WikitextContent(
760  wfMessage( 'category-move-redirect-override' )
761  ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
762  } else {
763  $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
764  $redirectContent = $contentHandler->makeRedirectContent( $nt,
765  wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
766  }
767 
768  // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
769  } else {
770  $redirectContent = null;
771  }
772 
773  // Figure out whether the content model is no longer the default
774  $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle );
775  $contentModel = $this->oldTitle->getContentModel();
776  $newDefault = ContentHandler::getDefaultModelFor( $nt );
777  $defaultContentModelChanging = ( $oldDefault !== $newDefault
778  && $oldDefault === $contentModel );
779 
780  // T59084: log_page should be the ID of the *moved* page
781  $oldid = $this->oldTitle->getArticleID();
782  $logTitle = clone $this->oldTitle;
783 
784  $logEntry = new ManualLogEntry( 'move', $logType );
785  $logEntry->setPerformer( $user );
786  $logEntry->setTarget( $logTitle );
787  $logEntry->setComment( $reason );
788  $logEntry->setParameters( [
789  '4::target' => $nt->getPrefixedText(),
790  '5::noredir' => $redirectContent ? '0' : '1',
791  ] );
792 
793  $formatter = LogFormatter::newFromEntry( $logEntry );
794  $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
795  $comment = $formatter->getPlainActionText();
796  if ( $reason ) {
797  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
798  }
799 
800  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
801 
802  $oldpage = WikiPage::factory( $this->oldTitle );
803  $oldcountable = $oldpage->isCountable();
804 
805  $newpage = WikiPage::factory( $nt );
806 
807  # Change the name of the target page:
808  $dbw->update( 'page',
809  /* SET */ [
810  'page_namespace' => $nt->getNamespace(),
811  'page_title' => $nt->getDBkey(),
812  ],
813  /* WHERE */ [ 'page_id' => $oldid ],
814  __METHOD__
815  );
816 
817  # Save a null revision in the page's history notifying of the move
818  $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
819  if ( !is_object( $nullRevision ) ) {
820  throw new MWException( 'Failed to create null revision while moving page ID '
821  . $oldid . ' to ' . $nt->getPrefixedDBkey() );
822  }
823 
824  $nullRevId = $nullRevision->insertOn( $dbw );
825  $logEntry->setAssociatedRevId( $nullRevId );
826 
832  $user->incEditCount();
833 
834  if ( !$redirectContent ) {
835  // Clean up the old title *before* reset article id - T47348
836  WikiPage::onArticleDelete( $this->oldTitle );
837  }
838 
839  $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
840  $nt->resetArticleID( $oldid );
841  $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
842 
843  $newpage->updateRevisionOn( $dbw, $nullRevision );
844 
845  Hooks::run( 'NewRevisionFromEditComplete',
846  [ $newpage, $nullRevision, $nullRevision->getParentId(), $user ] );
847 
848  $newpage->doEditUpdates( $nullRevision, $user,
849  [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
850 
851  // If the default content model changes, we need to populate rev_content_model
852  if ( $defaultContentModelChanging ) {
853  $dbw->update(
854  'revision',
855  [ 'rev_content_model' => $contentModel ],
856  [ 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ],
857  __METHOD__
858  );
859  }
860 
862 
863  # Recreate the redirect, this time in the other direction.
864  if ( $redirectContent ) {
865  $redirectArticle = WikiPage::factory( $this->oldTitle );
866  $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
867  $newid = $redirectArticle->insertOn( $dbw );
868  if ( $newid ) { // sanity
869  $this->oldTitle->resetArticleID( $newid );
870  $redirectRevision = new Revision( [
871  'title' => $this->oldTitle, // for determining the default content model
872  'page' => $newid,
873  'user_text' => $user->getName(),
874  'user' => $user->getId(),
875  'comment' => $comment,
876  'content' => $redirectContent ] );
877  $redirectRevId = $redirectRevision->insertOn( $dbw );
878  $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
879 
880  Hooks::run( 'NewRevisionFromEditComplete',
881  [ $redirectArticle, $redirectRevision, false, $user ] );
882 
883  $redirectArticle->doEditUpdates( $redirectRevision, $user, [ 'created' => true ] );
884 
885  // make a copy because of log entry below
886  $redirectTags = $changeTags;
887  if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
888  $redirectTags[] = 'mw-new-redirect';
889  }
890  ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
891  }
892  }
893 
894  # Log the move
895  $logid = $logEntry->insert();
896 
897  $logEntry->addTags( $changeTags );
898  $logEntry->publish( $logid );
899 
900  return $nullRevision;
901  }
902 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
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:3368
moveUnsafe(User $user, $reason, $createRedirect, array $changeTags)
Moves without any sort of safety or sanity checks.
Definition: MovePage.php:499
static singleton()
Definition: Collation.php:36
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter...
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title...
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with &#39;-&#39;.
hasSubpages( $index)
Does the namespace allow subpages?
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1858
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:138
const DB_MASTER
Definition: defines.php:26
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2229
moveIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Same as move(), but with permissions checks.
Definition: MovePage.php:339
A class for passing options to services.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
moveSubpages(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page&#39;s subpages to be subpages of the target page, without checking user permissions...
Definition: MovePage.php:377
__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
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
checkPermissions(User $user, $reason)
Check if the user is allowed to perform the move.
Definition: MovePage.php:121
ILoadBalancer $loadBalancer
Definition: MovePage.php:56
PermissionManager $permMgr
Definition: MovePage.php:71
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
Definition: MovePage.php:685
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
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:718
incEditCount()
Schedule a deferred update to update the user&#39;s edit count.
Definition: User.php:4956
const NS_CATEGORY
Definition: Defines.php:74
WatchedItemStoreInterface $watchedItems
Definition: MovePage.php:66
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
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
RepoGroup $repoGroup
Definition: MovePage.php:76
const NS_FILE
Definition: Defines.php:66
Content object for wiki text pages.
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
ServiceOptions $options
Definition: MovePage.php:51
insertOn( $dbw)
Insert a new revision into the database, returning the new revision ID number on success and dies hor...
Definition: Revision.php:948
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:612
isValidFileMove()
Sanity checks for when a file is being moved.
Definition: MovePage.php:237
static checkExtensionCompatibility(File $old, $new)
Checks if file extensions are compatible.
Definition: File.php:259
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Database cluster connection, tracking, load balancing, and transaction manager interface.
moveSubpagesInternal( $checkPermissions, User $user, $reason, $createRedirect, array $changeTags)
Definition: MovePage.php:410
moveSubpagesIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page&#39;s subpages to be subpages of the target page, with user permission checks...
Definition: MovePage.php:396
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they&#39;ve successfully logged in from...
Definition: User.php:4187
NamespaceInfo $nsInfo
Definition: MovePage.php:61
isValidMoveTarget()
Checks if $this can be moved to a given Title.
Definition: MovePage.php:267
getId()
Get the user&#39;s ID.
Definition: User.php:2200
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match...
Definition: EditPage.php:2505
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3404
move(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move a page without taking user permissions into account.
Definition: MovePage.php:319
getCanonicalName( $index)
Returns the canonical (English) name for a given index.
$content
Definition: router.php:78
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 $oldTitle
Definition: MovePage.php:41
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
isValidMove()
Does various sanity checks that the move is valid.
Definition: MovePage.php:163
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page&#39;s history.
Definition: Revision.php:994
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
Title $newTitle
Definition: MovePage.php:46
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200