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