Merge "Tweak checkbox sizes to be relative depending on where used."
[lhc/web/wiklou.git] / includes / MovePage.php
1 <?php
2
3 /**
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.
8 *
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.
13 *
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
18 *
19 * @file
20 */
21
22 /**
23 * Handles the backend logic of moving a page from one title
24 * to another.
25 *
26 * @since 1.24
27 */
28 class MovePage {
29
30 /**
31 * @var Title
32 */
33 protected $oldTitle;
34
35 /**
36 * @var Title
37 */
38 protected $newTitle;
39
40 public function __construct( Title $oldTitle, Title $newTitle ) {
41 $this->oldTitle = $oldTitle;
42 $this->newTitle = $newTitle;
43 }
44
45 /**
46 * Does various sanity checks that the move is
47 * valid. Only things based on the two titles
48 * should be checked here.
49 *
50 * @return Status
51 */
52 public function isValidMove() {
53 global $wgContentHandlerUseDB;
54 $status = new Status();
55
56 if ( $this->oldTitle->equals( $this->newTitle ) ) {
57 $status->fatal( 'selfmove' );
58 }
59 if ( !$this->oldTitle->isMovable() ) {
60 $status->fatal( 'immobile-source-namespace', $this->oldTitle->getNsText() );
61 }
62 if ( $this->newTitle->isExternal() ) {
63 $status->fatal( 'immobile-target-namespace-iw' );
64 }
65 if ( !$this->newTitle->isMovable() ) {
66 $status->fatal( 'immobile-target-namespace', $this->newTitle->getNsText() );
67 }
68
69 $oldid = $this->oldTitle->getArticleID();
70
71 if ( strlen( $this->newTitle->getDBkey() ) < 1 ) {
72 $errors[] = array( 'articleexists' );
73 }
74 if (
75 ( $this->oldTitle->getDBkey() == '' ) ||
76 ( !$oldid ) ||
77 ( $this->newTitle->getDBkey() == '' )
78 ) {
79 $errors[] = array( 'badarticleerror' );
80 }
81
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
86 $status->fatal(
87 'bad-target-model',
88 ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
89 ContentHandler::getLocalizedName( $this->newTitle->getContentModel() )
90 );
91 }
92
93 // Image-specific checks
94 if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
95 $status->merge( $this->isValidFileMove() );
96 }
97
98 if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
99 $status->fatal( 'nonfile-cannot-move-to-file' );
100 }
101
102 return $status;
103 }
104
105 /**
106 * Sanity checks for when a file is being moved
107 *
108 * @return Status
109 */
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' );
116 }
117 if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
118 $status->fatal( 'imagetypemismatch' );
119 }
120 }
121
122 if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
123 $status->fatal( 'imagenocrossnamespace' );
124 }
125
126 return $status;
127 }
128
129 /**
130 * @param User $user
131 * @param string $reason
132 * @param bool $createRedirect
133 * @return Status
134 */
135 public function move( User $user, $reason, $createRedirect ) {
136 global $wgCategoryCollation;
137
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() ) {
146 return $status;
147 }
148 }
149 // Clear RepoGroup process cache
150 RepoGroup::singleton()->clearCache( $this->oldTitle );
151 RepoGroup::singleton()->clearCache( $this->newTitle ); # clear false negative cache
152 }
153
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();
157
158 // Do the actual move
159 $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect );
160
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(
166 'categorylinks',
167 array( 'cl_sortkey_prefix', 'cl_to' ),
168 array( 'cl_from' => $pageid ),
169 __METHOD__
170 );
171 if ( $this->newTitle->getNamespace() == NS_CATEGORY ) {
172 $type = 'subcat';
173 } elseif ( $this->newTitle->getNamespace() == NS_FILE ) {
174 $type = 'file';
175 } else {
176 $type = 'page';
177 }
178 foreach ( $prefixes as $prefixRow ) {
179 $prefix = $prefixRow->cl_sortkey_prefix;
180 $catTo = $prefixRow->cl_to;
181 $dbw->update( 'categorylinks',
182 array(
183 'cl_sortkey' => Collation::singleton()->getSortKey(
184 $this->newTitle->getCategorySortkey( $prefix ) ),
185 'cl_collation' => $wgCategoryCollation,
186 'cl_type' => $type,
187 'cl_timestamp=cl_timestamp' ),
188 array(
189 'cl_from' => $pageid,
190 'cl_to' => $catTo ),
191 __METHOD__
192 );
193 }
194
195 $redirid = $this->oldTitle->getArticleID();
196
197 if ( $protected ) {
198 # Protect the redirect title as the title used to be...
199 $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
200 array(
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'
207 ),
208 array( 'pr_page' => $pageid ),
209 __METHOD__,
210 array( 'IGNORE' )
211 );
212 # Update the protection log
213 $log = new LogPage( 'protect' );
214 $comment = wfMessage(
215 'prot_1movedto2',
216 $this->oldTitle->getPrefixedText(),
217 $this->newTitle->getPrefixedText()
218 )->inContentLanguage()->text();
219 if ( $reason ) {
220 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
221 }
222 // @todo FIXME: $params?
223 $logId = $log->addEntry(
224 'move_prot',
225 $this->newTitle,
226 $comment,
227 array( $this->oldTitle->getPrefixedText() ),
228 $user
229 );
230
231 // reread inserted pr_ids for log relation
232 $insertedPrIds = $dbw->select(
233 'page_restrictions',
234 'pr_id',
235 array( 'pr_page' => $redirid ),
236 __METHOD__
237 );
238 $logRelationsValues = array();
239 foreach ( $insertedPrIds as $prid ) {
240 $logRelationsValues[] = $prid->pr_id;
241 }
242 $log->addRelations( 'pr_id', $logRelationsValues, $logId );
243 }
244
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 ),
250 __METHOD__
251 );
252 $dbw->update( 'templatelinks',
253 array( 'tl_from_namespace' => $this->newTitle->getNamespace() ),
254 array( 'tl_from' => $pageid ),
255 __METHOD__
256 );
257 $dbw->update( 'imagelinks',
258 array( 'il_from_namespace' => $this->newTitle->getNamespace() ),
259 array( 'il_from' => $pageid ),
260 __METHOD__
261 );
262 }
263
264 # Update watchlists
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 );
271 }
272
273 $dbw->commit( __METHOD__ );
274
275 wfRunHooks( 'TitleMoveComplete', array( &$this->oldTitle, &$this->newTitle, &$user, $pageid, $redirid, $reason ) );
276 return Status::newGood();
277
278 }
279
280 /**
281 * Move page to a title which is either a redirect to the
282 * source page or nonexistent
283 *
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
291 */
292 private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true ) {
293 global $wgContLang;
294
295 if ( $nt->exists() ) {
296 $moveOverRedirect = true;
297 $logType = 'move_redir';
298 } else {
299 $moveOverRedirect = false;
300 $logType = 'move';
301 }
302
303 if ( $createRedirect ) {
304 if ( $this->oldTitle->getNamespace() == NS_CATEGORY
305 && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
306 ) {
307 $redirectContent = new WikitextContent(
308 wfMessage( 'category-move-redirect-override' )
309 ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
310 } else {
311 $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
312 $redirectContent = $contentHandler->makeRedirectContent( $nt,
313 wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
314 }
315
316 // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
317 } else {
318 $redirectContent = null;
319 }
320
321 // bug 57084: log_page should be the ID of the *moved* page
322 $oldid = $this->oldTitle->getArticleID();
323 $logTitle = clone $this->oldTitle;
324
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',
332 ) );
333
334 $formatter = LogFormatter::newFromEntry( $logEntry );
335 $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
336 $comment = $formatter->getPlainActionText();
337 if ( $reason ) {
338 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
339 }
340 # Truncate for whole multibyte characters.
341 $comment = $wgContLang->truncate( $comment, 255 );
342
343 $dbw = wfGetDB( DB_MASTER );
344
345 $newpage = WikiPage::factory( $nt );
346
347 if ( $moveOverRedirect ) {
348 $newid = $nt->getArticleID();
349 $newcontent = $newpage->getContent();
350
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__ );
356
357 $newpage->doDeleteUpdates( $newid, $newcontent );
358 }
359
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__ );
364 }
365
366 $nullRevision->insertOn( $dbw );
367
368 # Change the name of the target page:
369 $dbw->update( 'page',
370 /* SET */ array(
371 'page_namespace' => $nt->getNamespace(),
372 'page_title' => $nt->getDBkey(),
373 ),
374 /* WHERE */ array( 'page_id' => $oldid ),
375 __METHOD__
376 );
377
378 // clean up the old title before reset article id - bug 45348
379 if ( !$redirectContent ) {
380 WikiPage::onArticleDelete( $this->oldTitle );
381 }
382
383 $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
384 $nt->resetArticleID( $oldid );
385 $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397
386
387 $newpage->updateRevisionOn( $dbw, $nullRevision );
388
389 wfRunHooks( 'NewRevisionFromEditComplete',
390 array( $newpage, $nullRevision, $nullRevision->getParentId(), $user ) );
391
392 $newpage->doEditUpdates( $nullRevision, $user, array( 'changed' => false ) );
393
394 if ( !$moveOverRedirect ) {
395 WikiPage::onArticleCreate( $nt );
396 }
397
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
407 'page' => $newid,
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 );
414
415 wfRunHooks( 'NewRevisionFromEditComplete',
416 array( $redirectArticle, $redirectRevision, false, $user ) );
417
418 $redirectArticle->doEditUpdates( $redirectRevision, $user, array( 'created' => true ) );
419 }
420 }
421
422 # Log the move
423 $logid = $logEntry->insert();
424 $logEntry->publish( $logid );
425 }
426
427 }