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