4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
23 * Handles the backend logic of moving a page from one title
40 public function __construct( Title
$oldTitle, Title
$newTitle ) {
41 $this->oldTitle
= $oldTitle;
42 $this->newTitle
= $newTitle;
46 * Does various sanity checks that the move is
47 * valid. Only things based on the two titles
48 * should be checked here.
52 public function isValidMove() {
53 global $wgContentHandlerUseDB;
54 $status = new Status();
56 if ( $this->oldTitle
->equals( $this->newTitle
) ) {
57 $status->fatal( 'selfmove' );
59 if ( !$this->oldTitle
->isMovable() ) {
60 $status->fatal( 'immobile-source-namespace', $this->oldTitle
->getNsText() );
62 if ( $this->newTitle
->isExternal() ) {
63 $status->fatal( 'immobile-target-namespace-iw' );
65 if ( !$this->newTitle
->isMovable() ) {
66 $status->fatal( 'immobile-target-namespace', $this->newTitle
->getNsText() );
69 $oldid = $this->oldTitle
->getArticleID();
71 if ( strlen( $this->newTitle
->getDBkey() ) < 1 ) {
72 $status->fatal( 'articleexists' );
75 ( $this->oldTitle
->getDBkey() == '' ) ||
77 ( $this->newTitle
->getDBkey() == '' )
79 $status->fatal( 'badarticleerror' );
82 // Content model checks
83 if ( !$wgContentHandlerUseDB &&
84 $this->oldTitle
->getContentModel() !== $this->newTitle
->getContentModel() ) {
85 // can't move a page if that would change the page's content model
88 ContentHandler
::getLocalizedName( $this->oldTitle
->getContentModel() ),
89 ContentHandler
::getLocalizedName( $this->newTitle
->getContentModel() )
93 // Image-specific checks
94 if ( $this->oldTitle
->inNamespace( NS_FILE
) ) {
95 $status->merge( $this->isValidFileMove() );
98 if ( $this->newTitle
->inNamespace( NS_FILE
) && !$this->oldTitle
->inNamespace( NS_FILE
) ) {
99 $status->fatal( 'nonfile-cannot-move-to-file' );
106 * Sanity checks for when a file is being moved
110 protected function isValidFileMove() {
111 $status = new Status();
112 $file = wfLocalFile( $this->oldTitle
);
113 if ( $file->exists() ) {
114 if ( $this->newTitle
->getText() != wfStripIllegalFilenameChars( $this->newTitle
->getText() ) ) {
115 $status->fatal( 'imageinvalidfilename' );
117 if ( !File
::checkExtensionCompatibility( $file, $this->newTitle
->getDBkey() ) ) {
118 $status->fatal( 'imagetypemismatch' );
122 if ( !$this->newTitle
->inNamespace( NS_FILE
) ) {
123 $status->fatal( 'imagenocrossnamespace' );
131 * @param string $reason
132 * @param bool $createRedirect
135 public function move( User
$user, $reason, $createRedirect ) {
136 global $wgCategoryCollation;
138 // If it is a file, move it first.
139 // It is done before all other moving stuff is done because it's hard to revert.
140 $dbw = wfGetDB( DB_MASTER
);
141 if ( $this->oldTitle
->getNamespace() == NS_FILE
) {
142 $file = wfLocalFile( $this->oldTitle
);
143 if ( $file->exists() ) {
144 $status = $file->move( $this->newTitle
);
145 if ( !$status->isOk() ) {
149 // Clear RepoGroup process cache
150 RepoGroup
::singleton()->clearCache( $this->oldTitle
);
151 RepoGroup
::singleton()->clearCache( $this->newTitle
); # clear false negative cache
154 $dbw->begin( __METHOD__
); # If $file was a LocalFile, its transaction would have closed our own.
155 $pageid = $this->oldTitle
->getArticleID( Title
::GAID_FOR_UPDATE
);
156 $protected = $this->oldTitle
->isProtected();
158 // Do the actual move
159 $this->moveToInternal( $user, $this->newTitle
, $reason, $createRedirect );
161 // Refresh the sortkey for this row. Be careful to avoid resetting
162 // cl_timestamp, which may disturb time-based lists on some sites.
163 // @todo This block should be killed, it's duplicating code
164 // from LinksUpdate::getCategoryInsertions() and friends.
165 $prefixes = $dbw->select(
167 array( 'cl_sortkey_prefix', 'cl_to' ),
168 array( 'cl_from' => $pageid ),
171 if ( $this->newTitle
->getNamespace() == NS_CATEGORY
) {
173 } elseif ( $this->newTitle
->getNamespace() == NS_FILE
) {
178 foreach ( $prefixes as $prefixRow ) {
179 $prefix = $prefixRow->cl_sortkey_prefix
;
180 $catTo = $prefixRow->cl_to
;
181 $dbw->update( 'categorylinks',
183 'cl_sortkey' => Collation
::singleton()->getSortKey(
184 $this->newTitle
->getCategorySortkey( $prefix ) ),
185 'cl_collation' => $wgCategoryCollation,
187 'cl_timestamp=cl_timestamp' ),
189 'cl_from' => $pageid,
195 $redirid = $this->oldTitle
->getArticleID();
198 # Protect the redirect title as the title used to be...
199 $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
201 'pr_page' => $redirid,
202 'pr_type' => 'pr_type',
203 'pr_level' => 'pr_level',
204 'pr_cascade' => 'pr_cascade',
205 'pr_user' => 'pr_user',
206 'pr_expiry' => 'pr_expiry'
208 array( 'pr_page' => $pageid ),
212 # Update the protection log
213 $log = new LogPage( 'protect' );
214 $comment = wfMessage(
216 $this->oldTitle
->getPrefixedText(),
217 $this->newTitle
->getPrefixedText()
218 )->inContentLanguage()->text();
220 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
222 // @todo FIXME: $params?
223 $logId = $log->addEntry(
227 array( $this->oldTitle
->getPrefixedText() ),
231 // reread inserted pr_ids for log relation
232 $insertedPrIds = $dbw->select(
235 array( 'pr_page' => $redirid ),
238 $logRelationsValues = array();
239 foreach ( $insertedPrIds as $prid ) {
240 $logRelationsValues[] = $prid->pr_id
;
242 $log->addRelations( 'pr_id', $logRelationsValues, $logId );
245 // Update *_from_namespace fields as needed
246 if ( $this->oldTitle
->getNamespace() != $this->newTitle
->getNamespace() ) {
247 $dbw->update( 'pagelinks',
248 array( 'pl_from_namespace' => $this->newTitle
->getNamespace() ),
249 array( 'pl_from' => $pageid ),
252 $dbw->update( 'templatelinks',
253 array( 'tl_from_namespace' => $this->newTitle
->getNamespace() ),
254 array( 'tl_from' => $pageid ),
257 $dbw->update( 'imagelinks',
258 array( 'il_from_namespace' => $this->newTitle
->getNamespace() ),
259 array( 'il_from' => $pageid ),
265 $oldtitle = $this->oldTitle
->getDBkey();
266 $newtitle = $this->newTitle
->getDBkey();
267 $oldsnamespace = MWNamespace
::getSubject( $this->oldTitle
->getNamespace() );
268 $newsnamespace = MWNamespace
::getSubject( $this->newTitle
->getNamespace() );
269 if ( $oldsnamespace != $newsnamespace ||
$oldtitle != $newtitle ) {
270 WatchedItem
::duplicateEntries( $this->oldTitle
, $this->newTitle
);
273 $dbw->commit( __METHOD__
);
275 wfRunHooks( 'TitleMoveComplete', array( &$this->oldTitle
, &$this->newTitle
, &$user, $pageid, $redirid, $reason ) );
276 return Status
::newGood();
281 * Move page to a title which is either a redirect to the
282 * source page or nonexistent
284 * @fixme This was basically directly moved from Title, it should be split into smaller functions
285 * @param User $user the User doing the move
286 * @param Title $nt The page to move to, which should be a redirect or nonexistent
287 * @param string $reason The reason for the move
288 * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
289 * if the user has the suppressredirect right
290 * @throws MWException
292 private function moveToInternal( User
$user, &$nt, $reason = '', $createRedirect = true ) {
295 if ( $nt->exists() ) {
296 $moveOverRedirect = true;
297 $logType = 'move_redir';
299 $moveOverRedirect = false;
303 if ( $createRedirect ) {
304 if ( $this->oldTitle
->getNamespace() == NS_CATEGORY
305 && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
307 $redirectContent = new WikitextContent(
308 wfMessage( 'category-move-redirect-override' )
309 ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
311 $contentHandler = ContentHandler
::getForTitle( $this->oldTitle
);
312 $redirectContent = $contentHandler->makeRedirectContent( $nt,
313 wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
316 // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
318 $redirectContent = null;
321 // bug 57084: log_page should be the ID of the *moved* page
322 $oldid = $this->oldTitle
->getArticleID();
323 $logTitle = clone $this->oldTitle
;
325 $logEntry = new ManualLogEntry( 'move', $logType );
326 $logEntry->setPerformer( $user );
327 $logEntry->setTarget( $logTitle );
328 $logEntry->setComment( $reason );
329 $logEntry->setParameters( array(
330 '4::target' => $nt->getPrefixedText(),
331 '5::noredir' => $redirectContent ?
'0': '1',
334 $formatter = LogFormatter
::newFromEntry( $logEntry );
335 $formatter->setContext( RequestContext
::newExtraneousContext( $this->oldTitle
) );
336 $comment = $formatter->getPlainActionText();
338 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
340 # Truncate for whole multibyte characters.
341 $comment = $wgContLang->truncate( $comment, 255 );
343 $dbw = wfGetDB( DB_MASTER
);
345 $newpage = WikiPage
::factory( $nt );
347 if ( $moveOverRedirect ) {
348 $newid = $nt->getArticleID();
349 $newcontent = $newpage->getContent();
351 # Delete the old redirect. We don't save it to history since
352 # by definition if we've got here it's rather uninteresting.
353 # We have to remove it so that the next step doesn't trigger
354 # a conflict on the unique namespace+title index...
355 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__
);
357 $newpage->doDeleteUpdates( $newid, $newcontent );
360 # Save a null revision in the page's history notifying of the move
361 $nullRevision = Revision
::newNullRevision( $dbw, $oldid, $comment, true, $user );
362 if ( !is_object( $nullRevision ) ) {
363 throw new MWException( 'No valid null revision produced in ' . __METHOD__
);
366 $nullRevision->insertOn( $dbw );
368 # Change the name of the target page:
369 $dbw->update( 'page',
371 'page_namespace' => $nt->getNamespace(),
372 'page_title' => $nt->getDBkey(),
374 /* WHERE */ array( 'page_id' => $oldid ),
378 // clean up the old title before reset article id - bug 45348
379 if ( !$redirectContent ) {
380 WikiPage
::onArticleDelete( $this->oldTitle
);
383 $this->oldTitle
->resetArticleID( 0 ); // 0 == non existing
384 $nt->resetArticleID( $oldid );
385 $newpage->loadPageData( WikiPage
::READ_LOCKING
); // bug 46397
387 $newpage->updateRevisionOn( $dbw, $nullRevision );
389 wfRunHooks( 'NewRevisionFromEditComplete',
390 array( $newpage, $nullRevision, $nullRevision->getParentId(), $user ) );
392 $newpage->doEditUpdates( $nullRevision, $user, array( 'changed' => false ) );
394 if ( !$moveOverRedirect ) {
395 WikiPage
::onArticleCreate( $nt );
398 # Recreate the redirect, this time in the other direction.
399 if ( $redirectContent ) {
400 $redirectArticle = WikiPage
::factory( $this->oldTitle
);
401 $redirectArticle->loadFromRow( false, WikiPage
::READ_LOCKING
); // bug 46397
402 $newid = $redirectArticle->insertOn( $dbw );
403 if ( $newid ) { // sanity
404 $this->oldTitle
->resetArticleID( $newid );
405 $redirectRevision = new Revision( array(
406 'title' => $this->oldTitle
, // for determining the default content model
408 'user_text' => $user->getName(),
409 'user' => $user->getId(),
410 'comment' => $comment,
411 'content' => $redirectContent ) );
412 $redirectRevision->insertOn( $dbw );
413 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
415 wfRunHooks( 'NewRevisionFromEditComplete',
416 array( $redirectArticle, $redirectRevision, false, $user ) );
418 $redirectArticle->doEditUpdates( $redirectRevision, $user, array( 'created' => true ) );
423 $logid = $logEntry->insert();
424 $logEntry->publish( $logid );