MediaWiki  master
MovePage.php
Go to the documentation of this file.
1 <?php
2 
32 
39 class MovePage {
40 
44  protected $oldTitle;
45 
49  protected $newTitle;
50 
54  protected $options;
55 
59  protected $loadBalancer;
60 
64  protected $nsInfo;
65 
69  protected $watchedItems;
70 
74  protected $permMgr;
75 
79  protected $repoGroup;
80 
85 
89  private $revisionStore;
90 
105  public function __construct(
108  ServiceOptions $options = null,
110  NamespaceInfo $nsInfo = null,
113  RepoGroup $repoGroup = null,
116  ) {
117  $this->oldTitle = $oldTitle;
118  $this->newTitle = $newTitle;
119  $this->options = $options ??
120  new ServiceOptions( MovePageFactory::CONSTRUCTOR_OPTIONS,
121  MediaWikiServices::getInstance()->getMainConfig() );
122  $this->loadBalancer =
123  $loadBalancer ?? MediaWikiServices::getInstance()->getDBLoadBalancer();
124  $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
125  $this->watchedItems =
126  $watchedItems ?? MediaWikiServices::getInstance()->getWatchedItemStore();
127  $this->permMgr = $permMgr ?? MediaWikiServices::getInstance()->getPermissionManager();
128  $this->repoGroup = $repoGroup ?? MediaWikiServices::getInstance()->getRepoGroup();
129  $this->contentHandlerFactory = (
130  $contentHandlerFactory ?? MediaWikiServices::getInstance()->getContentHandlerFactory()
131  );
132  $this->revisionStore =
133  $revisionStore ?? MediaWikiServices::getInstance()->getRevisionStore();
134  }
135 
144  public function checkPermissions( User $user, $reason ) {
145  $status = new Status();
146 
147  $errors = wfMergeErrorArrays(
148  $this->permMgr->getPermissionErrors( 'move', $user, $this->oldTitle ),
149  $this->permMgr->getPermissionErrors( 'edit', $user, $this->oldTitle ),
150  $this->permMgr->getPermissionErrors( 'move-target', $user, $this->newTitle ),
151  $this->permMgr->getPermissionErrors( 'edit', $user, $this->newTitle )
152  );
153 
154  // Convert into a Status object
155  if ( $errors ) {
156  foreach ( $errors as $error ) {
157  $status->fatal( ...$error );
158  }
159  }
160 
161  if ( $reason !== null && EditPage::matchSummarySpamRegex( $reason ) !== false ) {
162  // This is kind of lame, won't display nice
163  $status->fatal( 'spamprotectiontext' );
164  }
165 
166  $tp = $this->newTitle->getTitleProtection();
167  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
168  if ( $tp !== false && !$permissionManager->userHasRight( $user, $tp['permission'] ) ) {
169  $status->fatal( 'cantmove-titleprotected' );
170  }
171 
172  Hooks::run( 'MovePageCheckPermissions',
173  [ $this->oldTitle, $this->newTitle, $user, $reason, $status ]
174  );
175 
176  return $status;
177  }
178 
186  public function isValidMove() {
187  $status = new Status();
188 
189  if ( $this->oldTitle->equals( $this->newTitle ) ) {
190  $status->fatal( 'selfmove' );
191  } elseif ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) {
192  // The move is allowed only if (1) the target doesn't exist, or (2) the target is a
193  // redirect to the source, and has no history (so we can undo bad moves right after
194  // they're done).
195  $status->fatal( 'articleexists', $this->newTitle->getPrefixedText() );
196  }
197 
198  // @todo If the old title is invalid, maybe we should check if it somehow exists in the
199  // database and allow moving it to a valid name? Why prohibit the move from an empty name
200  // without checking in the database?
201  if ( $this->oldTitle->getDBkey() == '' ) {
202  $status->fatal( 'badarticleerror' );
203  } elseif ( $this->oldTitle->isExternal() ) {
204  $status->fatal( 'immobile-source-namespace-iw' );
205  } elseif ( !$this->oldTitle->isMovable() ) {
206  $status->fatal( 'immobile-source-namespace', $this->oldTitle->getNsText() );
207  } elseif ( !$this->oldTitle->exists() ) {
208  $status->fatal( 'movepage-source-doesnt-exist' );
209  }
210 
211  if ( $this->newTitle->isExternal() ) {
212  $status->fatal( 'immobile-target-namespace-iw' );
213  } elseif ( !$this->newTitle->isMovable() ) {
214  $status->fatal( 'immobile-target-namespace', $this->newTitle->getNsText() );
215  }
216  if ( !$this->newTitle->isValid() ) {
217  $status->fatal( 'movepage-invalid-target-title' );
218  }
219 
220  // Content model checks
221  if ( !$this->contentHandlerFactory
222  ->getContentHandler( $this->oldTitle->getContentModel() )
223  ->canBeUsedOn( $this->newTitle )
224  ) {
225  $status->fatal(
226  'content-not-allowed-here',
227  ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
228  $this->newTitle->getPrefixedText(),
229  SlotRecord::MAIN
230  );
231  }
232 
233  // Image-specific checks
234  if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
235  $status->merge( $this->isValidFileMove() );
236  }
237 
238  if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
239  $status->fatal( 'nonfile-cannot-move-to-file' );
240  }
241 
242  // Hook for extensions to say a title can't be moved for technical reasons
243  Hooks::run( 'MovePageIsValidMove', [ $this->oldTitle, $this->newTitle, $status ] );
244 
245  return $status;
246  }
247 
253  protected function isValidFileMove() {
254  $status = new Status();
255 
256  if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
257  $status->fatal( 'imagenocrossnamespace' );
258  // No need for further errors about the target filename being wrong
259  return $status;
260  }
261 
262  $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
263  $file->load( File::READ_LATEST );
264  if ( $file->exists() ) {
265  if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) {
266  $status->fatal( 'imageinvalidfilename' );
267  }
268  if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
269  $status->fatal( 'imagetypemismatch' );
270  }
271  }
272 
273  return $status;
274  }
275 
283  protected function isValidMoveTarget() {
284  # Is it an existing file?
285  if ( $this->newTitle->inNamespace( NS_FILE ) ) {
286  $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
287  $file->load( File::READ_LATEST );
288  if ( $file->exists() ) {
289  wfDebug( __METHOD__ . ": file exists\n" );
290  return false;
291  }
292  }
293  # Is it a redirect with no history?
294  if ( !$this->newTitle->isSingleRevRedirect() ) {
295  wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
296  return false;
297  }
298  # Get the article text
299  $rev = $this->revisionStore->getRevisionByTitle(
300  $this->newTitle,
301  0,
302  RevisionStore::READ_LATEST
303  );
304  if ( !is_object( $rev ) ) {
305  return false;
306  }
307  $content = $rev->getContent( SlotRecord::MAIN );
308  # Does the redirect point to the source?
309  # Or is it a broken self-redirect, usually caused by namespace collisions?
310  $redirTitle = $content ? $content->getRedirectTarget() : null;
311 
312  if ( $redirTitle ) {
313  if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
314  $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
315  wfDebug( __METHOD__ . ": redirect points to other page\n" );
316  return false;
317  } else {
318  return true;
319  }
320  } else {
321  # Fail safe (not a redirect after all. strange.)
322  wfDebug( __METHOD__ . ": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
323  " is a redirect, but it doesn't contain a valid redirect.\n" );
324  return false;
325  }
326  }
327 
339  public function move(
340  User $user, $reason = null, $createRedirect = true, array $changeTags = []
341  ) {
342  $status = $this->isValidMove();
343  if ( !$status->isOK() ) {
344  return $status;
345  }
346 
347  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
348  }
349 
359  public function moveIfAllowed(
360  User $user, $reason = null, $createRedirect = true, array $changeTags = []
361  ) {
362  $status = $this->isValidMove();
363  $status->merge( $this->checkPermissions( $user, $reason ) );
364  if ( $changeTags ) {
365  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $user ) );
366  }
367 
368  if ( !$status->isOK() ) {
369  // Auto-block user's IP if the account was "hard" blocked
370  $user->spreadAnyEditBlock();
371  return $status;
372  }
373 
374  // Check suppressredirect permission
375  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
376  if ( !$permissionManager->userHasRight( $user, 'suppressredirect' ) ) {
377  $createRedirect = true;
378  }
379 
380  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
381  }
382 
397  public function moveSubpages(
398  User $user, $reason = null, $createRedirect = true, array $changeTags = []
399  ) {
400  return $this->moveSubpagesInternal( false, $user, $reason, $createRedirect, $changeTags );
401  }
402 
416  public function moveSubpagesIfAllowed(
417  User $user, $reason = null, $createRedirect = true, array $changeTags = []
418  ) {
419  return $this->moveSubpagesInternal( true, $user, $reason, $createRedirect, $changeTags );
420  }
421 
430  private function moveSubpagesInternal(
431  $checkPermissions, User $user, $reason, $createRedirect, array $changeTags
432  ) {
433  global $wgMaximumMovedPages;
434  $services = MediaWikiServices::getInstance();
435 
436  if ( $checkPermissions ) {
437  if ( !$services->getPermissionManager()->userCan(
438  'move-subpages', $user, $this->oldTitle )
439  ) {
440  return Status::newFatal( 'cant-move-subpages' );
441  }
442  }
443 
444  $nsInfo = $services->getNamespaceInfo();
445 
446  // Do the source and target namespaces support subpages?
447  if ( !$nsInfo->hasSubpages( $this->oldTitle->getNamespace() ) ) {
448  return Status::newFatal( 'namespace-nosubpages',
449  $nsInfo->getCanonicalName( $this->oldTitle->getNamespace() ) );
450  }
451  if ( !$nsInfo->hasSubpages( $this->newTitle->getNamespace() ) ) {
452  return Status::newFatal( 'namespace-nosubpages',
453  $nsInfo->getCanonicalName( $this->newTitle->getNamespace() ) );
454  }
455 
456  // Return a status for the overall result. Its value will be an array with per-title
457  // status for each subpage. Merge any errors from the per-title statuses into the
458  // top-level status without resetting the overall result.
459  $topStatus = Status::newGood();
460  $perTitleStatus = [];
461  $subpages = $this->oldTitle->getSubpages( $wgMaximumMovedPages + 1 );
462  $count = 0;
463  foreach ( $subpages as $oldSubpage ) {
464  $count++;
465  if ( $count > $wgMaximumMovedPages ) {
466  $status = Status::newFatal( 'movepage-max-pages', $wgMaximumMovedPages );
467  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
468  $topStatus->merge( $status );
469  $topStatus->setOK( true );
470  break;
471  }
472 
473  // We don't know whether this function was called before or after moving the root page,
474  // so check both titles
475  if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
476  $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
477  ) {
478  // When moving a page to a subpage of itself, don't move it twice
479  continue;
480  }
481  $newPageName = preg_replace(
482  '#^' . preg_quote( $this->oldTitle->getDBkey(), '#' ) . '#',
483  StringUtils::escapeRegexReplacement( $this->newTitle->getDBkey() ), # T23234
484  $oldSubpage->getDBkey() );
485  if ( $oldSubpage->isTalkPage() ) {
486  $newNs = $this->newTitle->getTalkPage()->getNamespace();
487  } else {
488  $newNs = $this->newTitle->getSubjectPage()->getNamespace();
489  }
490  // T16385: we need makeTitleSafe because the new page names may be longer than 255
491  // characters.
492  $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
493 
494  $mp = new MovePage( $oldSubpage, $newSubpage );
495  $method = $checkPermissions ? 'moveIfAllowed' : 'move';
497  $status = $mp->$method( $user, $reason, $createRedirect, $changeTags );
498  if ( $status->isOK() ) {
499  $status->setResult( true, $newSubpage->getPrefixedText() );
500  }
501  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
502  $topStatus->merge( $status );
503  $topStatus->setOK( true );
504  }
505 
506  $topStatus->value = $perTitleStatus;
507  return $topStatus;
508  }
509 
519  private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) {
520  $status = Status::newGood();
521  Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user, $reason, &$status ] );
522  if ( !$status->isOK() ) {
523  // Move was aborted by the hook
524  return $status;
525  }
526 
527  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
528  $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
529 
530  Hooks::run( 'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] );
531 
532  $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
533  $protected = $this->oldTitle->isProtected();
534 
535  // Do the actual move; if this fails, it will throw an MWException(!)
536  $nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
537  $changeTags );
538 
539  // Refresh the sortkey for this row. Be careful to avoid resetting
540  // cl_timestamp, which may disturb time-based lists on some sites.
541  // @todo This block should be killed, it's duplicating code
542  // from LinksUpdate::getCategoryInsertions() and friends.
543  $prefixes = $dbw->select(
544  'categorylinks',
545  [ 'cl_sortkey_prefix', 'cl_to' ],
546  [ 'cl_from' => $pageid ],
547  __METHOD__
548  );
549  $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() );
550  foreach ( $prefixes as $prefixRow ) {
551  $prefix = $prefixRow->cl_sortkey_prefix;
552  $catTo = $prefixRow->cl_to;
553  $dbw->update( 'categorylinks',
554  [
555  'cl_sortkey' => Collation::singleton()->getSortKey(
556  $this->newTitle->getCategorySortkey( $prefix ) ),
557  'cl_collation' => $this->options->get( 'CategoryCollation' ),
558  'cl_type' => $type,
559  'cl_timestamp=cl_timestamp' ],
560  [
561  'cl_from' => $pageid,
562  'cl_to' => $catTo ],
563  __METHOD__
564  );
565  }
566 
567  $redirid = $this->oldTitle->getArticleID();
568 
569  if ( $protected ) {
570  # Protect the redirect title as the title used to be...
571  $res = $dbw->select(
572  'page_restrictions',
573  [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
574  [ 'pr_page' => $pageid ],
575  __METHOD__,
576  'FOR UPDATE'
577  );
578  $rowsInsert = [];
579  foreach ( $res as $row ) {
580  $rowsInsert[] = [
581  'pr_page' => $redirid,
582  'pr_type' => $row->pr_type,
583  'pr_level' => $row->pr_level,
584  'pr_cascade' => $row->pr_cascade,
585  'pr_user' => $row->pr_user,
586  'pr_expiry' => $row->pr_expiry
587  ];
588  }
589  $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
590 
591  // Build comment for log
592  $comment = wfMessage(
593  'prot_1movedto2',
594  $this->oldTitle->getPrefixedText(),
595  $this->newTitle->getPrefixedText()
596  )->inContentLanguage()->text();
597  if ( $reason ) {
598  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
599  }
600 
601  // reread inserted pr_ids for log relation
602  $insertedPrIds = $dbw->select(
603  'page_restrictions',
604  'pr_id',
605  [ 'pr_page' => $redirid ],
606  __METHOD__
607  );
608  $logRelationsValues = [];
609  foreach ( $insertedPrIds as $prid ) {
610  $logRelationsValues[] = $prid->pr_id;
611  }
612 
613  // Update the protection log
614  $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
615  $logEntry->setTarget( $this->newTitle );
616  $logEntry->setComment( $comment );
617  $logEntry->setPerformer( $user );
618  $logEntry->setParameters( [
619  '4::oldtitle' => $this->oldTitle->getPrefixedText(),
620  ] );
621  $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
622  $logEntry->addTags( $changeTags );
623  $logId = $logEntry->insert();
624  $logEntry->publish( $logId );
625  }
626 
627  // Update *_from_namespace fields as needed
628  if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
629  $dbw->update( 'pagelinks',
630  [ 'pl_from_namespace' => $this->newTitle->getNamespace() ],
631  [ 'pl_from' => $pageid ],
632  __METHOD__
633  );
634  $dbw->update( 'templatelinks',
635  [ 'tl_from_namespace' => $this->newTitle->getNamespace() ],
636  [ 'tl_from' => $pageid ],
637  __METHOD__
638  );
639  $dbw->update( 'imagelinks',
640  [ 'il_from_namespace' => $this->newTitle->getNamespace() ],
641  [ 'il_from' => $pageid ],
642  __METHOD__
643  );
644  }
645 
646  # Update watchlists
647  $oldtitle = $this->oldTitle->getDBkey();
648  $newtitle = $this->newTitle->getDBkey();
649  $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
650  $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
651  if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
652  $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
653  }
654 
655  // If it is a file then move it last.
656  // This is done after all database changes so that file system errors cancel the transaction.
657  if ( $this->oldTitle->getNamespace() == NS_FILE ) {
658  $status = $this->moveFile( $this->oldTitle, $this->newTitle );
659  if ( !$status->isOK() ) {
660  $dbw->cancelAtomic( __METHOD__ );
661  return $status;
662  }
663  }
664 
665  $nullRevisionObj = new Revision( $nullRevision );
666  Hooks::run(
667  'TitleMoveCompleting',
668  [ $this->oldTitle, $this->newTitle,
669  $user, $pageid, $redirid, $reason, $nullRevisionObj ]
670  );
671 
672  $dbw->endAtomic( __METHOD__ );
673 
674  // Keep each single hook handler atomic
677  $dbw,
678  __METHOD__,
679  function () use ( $user, $pageid, $redirid, $reason, $nullRevisionObj ) {
680  Hooks::run( 'TitleMoveComplete', [
681  &$this->oldTitle,
682  &$this->newTitle,
683  &$user,
684  $pageid,
685  $redirid,
686  $reason,
687  $nullRevisionObj
688  ] );
689  }
690  )
691  );
692 
693  return Status::newGood();
694  }
695 
705  private function moveFile( $oldTitle, $newTitle ) {
706  $status = Status::newFatal(
707  'cannotdelete',
709  );
710 
711  $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle );
712  $file->load( File::READ_LATEST );
713  if ( $file->exists() ) {
714  $status = $file->move( $newTitle );
715  }
716 
717  // Clear RepoGroup process cache
718  $this->repoGroup->clearCache( $oldTitle );
719  $this->repoGroup->clearCache( $newTitle ); # clear false negative cache
720  return $status;
721  }
722 
738  private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true,
739  array $changeTags = []
740  ) {
741  if ( $nt->exists() ) {
742  $moveOverRedirect = true;
743  $logType = 'move_redir';
744  } else {
745  $moveOverRedirect = false;
746  $logType = 'move';
747  }
748 
749  if ( $moveOverRedirect ) {
750  $overwriteMessage = wfMessage(
751  'delete_and_move_reason',
752  $this->oldTitle->getPrefixedText()
753  )->inContentLanguage()->text();
754  $newpage = WikiPage::factory( $nt );
755  $errs = [];
756  $status = $newpage->doDeleteArticleReal(
757  $overwriteMessage,
758  $user,
759  /* $suppress */ false,
760  /* unused */ null,
761  $errs,
762  /* unused */ null,
763  $changeTags,
764  'delete_redir'
765  );
766 
767  if ( !$status->isGood() ) {
768  throw new MWException( 'Failed to delete page-move revision: '
769  . $status->getWikiText( false, false, 'en' ) );
770  }
771 
772  $nt->resetArticleID( false );
773  }
774 
775  if ( $createRedirect ) {
776  if ( $this->oldTitle->getNamespace() == NS_CATEGORY
777  && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
778  ) {
779  $redirectContent = new WikitextContent(
780  wfMessage( 'category-move-redirect-override' )
781  ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
782  } else {
783  $redirectContent = $this->contentHandlerFactory
784  ->getContentHandler( $this->oldTitle->getContentModel() )
785  ->makeRedirectContent(
786  $nt,
787  wfMessage( 'move-redirect-text' )->inContentLanguage()->plain()
788  );
789  }
790 
791  // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
792  } else {
793  $redirectContent = null;
794  }
795 
796  // T59084: log_page should be the ID of the *moved* page
797  $oldid = $this->oldTitle->getArticleID();
798  $logTitle = clone $this->oldTitle;
799 
800  $logEntry = new ManualLogEntry( 'move', $logType );
801  $logEntry->setPerformer( $user );
802  $logEntry->setTarget( $logTitle );
803  $logEntry->setComment( $reason );
804  $logEntry->setParameters( [
805  '4::target' => $nt->getPrefixedText(),
806  '5::noredir' => $redirectContent ? '0' : '1',
807  ] );
808 
809  $formatter = LogFormatter::newFromEntry( $logEntry );
810  $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
811  $comment = $formatter->getPlainActionText();
812  if ( $reason ) {
813  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
814  }
815 
816  $dbw = $this->loadBalancer->getConnection( DB_MASTER );
817 
818  $oldpage = WikiPage::factory( $this->oldTitle );
819  $oldcountable = $oldpage->isCountable();
820 
821  $newpage = WikiPage::factory( $nt );
822 
823  # Change the name of the target page:
824  $dbw->update( 'page',
825  /* SET */ [
826  'page_namespace' => $nt->getNamespace(),
827  'page_title' => $nt->getDBkey(),
828  ],
829  /* WHERE */ [ 'page_id' => $oldid ],
830  __METHOD__
831  );
832 
833  # Save a null revision in the page's history notifying of the move
834  $nullRevision = $this->revisionStore->newNullRevision(
835  $dbw,
836  $this->oldTitle,
838  true,
839  $user
840  );
841  if ( !is_object( $nullRevision ) ) {
842  throw new MWException( 'Failed to create null revision while moving page ID '
843  . $oldid . ' to ' . $nt->getPrefixedDBkey() );
844  }
845 
846  $nullRevision = $this->revisionStore->insertRevisionOn( $nullRevision, $dbw );
847  $logEntry->setAssociatedRevId( $nullRevision->getId() );
848 
854  $user->incEditCount();
855 
856  if ( !$redirectContent ) {
857  // Clean up the old title *before* reset article id - T47348
858  WikiPage::onArticleDelete( $this->oldTitle );
859  }
860 
861  $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
862  $nt->resetArticleID( $oldid );
863  $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
864 
865  $nullRevisionObj = new Revision( $nullRevision );
866  $newpage->updateRevisionOn( $dbw, $nullRevisionObj );
867 
868  $fakeTags = [];
869  Hooks::run( 'NewRevisionFromEditComplete',
870  [ $newpage, $nullRevisionObj, $nullRevision->getParentId(), $user, &$fakeTags ] );
871 
872  $newpage->doEditUpdates( $nullRevisionObj, $user,
873  [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
874 
876 
877  # Recreate the redirect, this time in the other direction.
878  if ( $redirectContent ) {
879  $redirectArticle = WikiPage::factory( $this->oldTitle );
880  $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
881  $newid = $redirectArticle->insertOn( $dbw );
882  if ( $newid ) { // sanity
883  $this->oldTitle->resetArticleID( $newid );
884  $redirectRevision = new Revision( [
885  'title' => $this->oldTitle, // for determining the default content model
886  'page' => $newid,
887  'user_text' => $user->getName(),
888  'user' => $user->getId(),
889  'comment' => $comment,
890  'content' => $redirectContent ] );
891  $redirectRevId = $redirectRevision->insertOn( $dbw );
892  $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
893 
894  $fakeTags = [];
895  Hooks::run( 'NewRevisionFromEditComplete',
896  [ $redirectArticle, $redirectRevision, false, $user, &$fakeTags ] );
897 
898  $redirectArticle->doEditUpdates( $redirectRevision, $user, [ 'created' => true ] );
899 
900  // make a copy because of log entry below
901  $redirectTags = $changeTags;
902  if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
903  $redirectTags[] = 'mw-new-redirect';
904  }
905  ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
906  }
907  }
908 
909  # Log the move
910  $logid = $logEntry->insert();
911 
912  $logEntry->addTags( $changeTags );
913  $logEntry->publish( $logid );
914 
915  return $nullRevision;
916  }
917 }
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
Definition: GlobalFunctions.php:181
MovePage\checkPermissions
checkPermissions(User $user, $reason)
Check if the user is allowed to perform the move.
Definition: MovePage.php:144
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:3378
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
MovePage\$loadBalancer
ILoadBalancer $loadBalancer
Definition: MovePage.php:59
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:2158
$wgMaximumMovedPages
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
Definition: DefaultSettings.php:8806
MovePage\isValidMoveTarget
isValidMoveTarget()
Checks if $this can be moved to a given Title.
Definition: MovePage.php:283
MovePage\move
move(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move a page without taking user permissions into account.
Definition: MovePage.php:339
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:137
MovePage\$oldTitle
Title $oldTitle
Definition: MovePage.php:44
User\incEditCount
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition: User.php:4956
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:77
MovePage\$revisionStore
RevisionStore $revisionStore
Definition: MovePage.php:89
User\spreadAnyEditBlock
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
Definition: User.php:4185
MovePage\moveFile
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
Definition: MovePage.php:705
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1852
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:186
StringUtils\escapeRegexReplacement
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Definition: StringUtils.php:312
NS_FILE
const NS_FILE
Definition: Defines.php:75
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1201
RequestContext\newExtraneousContext
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
Definition: RequestContext.php:621
$res
$res
Definition: testCompression.php:57
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:947
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:457
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:42
Collation\singleton
static singleton()
Definition: Collation.php:36
MovePage\__construct
__construct(Title $oldTitle, Title $newTitle, ServiceOptions $options=null, ILoadBalancer $loadBalancer=null, NamespaceInfo $nsInfo=null, WatchedItemStoreInterface $watchedItems=null, PermissionManager $permMgr=null, RepoGroup $repoGroup=null, IContentHandlerFactory $contentHandlerFactory=null, RevisionStore $revisionStore=null)
Calling this directly is deprecated in 1.34.
Definition: MovePage.php:105
Revision
Definition: Revision.php:40
MovePage\$options
ServiceOptions $options
Definition: MovePage.php:54
MWException
MediaWiki exception.
Definition: MWException.php:26
wfStripIllegalFilenameChars
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
Definition: GlobalFunctions.php:2678
AbstractContent\getContentHandler
getContentHandler()
Definition: AbstractContent.php:91
MovePage\$repoGroup
RepoGroup $repoGroup
Definition: MovePage.php:79
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:143
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:39
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:416
MovePage\$nsInfo
NamespaceInfo $nsInfo
Definition: MovePage.php:64
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:3414
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:63
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
MovePage\moveSubpages
moveSubpages(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, without checking user permissions.
Definition: MovePage.php:397
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:38
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:621
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:48
$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:359
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:293
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
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:2603
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:738
MovePage\$newTitle
Title $newTitle
Definition: MovePage.php:49
MovePage\$watchedItems
WatchedItemStoreInterface $watchedItems
Definition: MovePage.php:69
MovePage\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: MovePage.php:84
MovePage\moveUnsafe
moveUnsafe(User $user, $reason, $createRedirect, array $changeTags)
Moves without any sort of safety or sanity checks.
Definition: MovePage.php:519
NamespaceInfo\getCanonicalName
getCanonicalName( $index)
Returns the canonical (English) name for a given index.
Definition: NamespaceInfo.php:362
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:528
Title
Represents a title within MediaWiki.
Definition: Title.php:42
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:253
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:38
MovePage\$permMgr
PermissionManager $permMgr
Definition: MovePage.php:74
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:53
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:2187
MovePage\moveSubpagesInternal
moveSubpagesInternal( $checkPermissions, User $user, $reason, $createRedirect, array $changeTags)
Definition: MovePage.php:430
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:256
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:52