r86001, now with less scariness :P I took out the delete action and did purge instea...
[lhc/web/wiklou.git] / includes / actions / DeleteAction.php
1 <?php
2 /**
3 * Performs the watch and unwatch actions on a page
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18 *
19 * @file
20 * @ingroup Actions
21 */
22
23 class DeleteAction extends Action {
24
25 public function getName(){
26 return 'delete';
27 }
28
29 public function getRestriction(){
30 return 'delete';
31 }
32
33 protected function getDescription(){
34 return wfMsg( 'delete-confirm', $this->getTitle()->getPrefixedText() );
35 }
36
37 /**
38 * Check that the deletion can be executed. In addition to checking the user permissions,
39 * check that the page is not too big and has not already been deleted.
40 * @throws ErrorPageError
41 * @see Action::checkCanExecute
42 */
43 protected function checkCanExecute( User $user ){
44
45 // Check that the article hasn't already been deleted
46 $dbw = wfGetDB( DB_MASTER );
47 $conds = $this->getTitle()->pageCond();
48 $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
49 if ( $latest === false ) {
50 // Get the deletion log
51 $log = '';
52 LogEventsList::showLogExtract(
53 $log,
54 'delete',
55 $this->getTitle()->getPrefixedText()
56 );
57
58 $msg = new Message( 'cannotdelete' );
59 $msg->params( $this->getTitle()->getPrefixedText() ); // This parameter is parsed
60 $msg->rawParams( $log ); // This is not
61
62 throw new ErrorPageError( 'internalerror', $msg );
63 }
64
65 // Limit deletions of big pages
66 $bigHistory = $this->isBigDeletion();
67 if ( $bigHistory && !$user->isAllowed( 'bigdelete' ) ) {
68 global $wgDeleteRevisionsLimit;
69 throw new ErrorPageError(
70 'internalerror',
71 'delete-toobig',
72 $this->getContext()->lang->formatNum( $wgDeleteRevisionsLimit )
73 );
74 }
75
76 return parent::checkCanExecute( $user );
77 }
78
79 protected function getFormFields(){
80 // TODO: add more useful things here?
81 $infoText = Html::rawElement(
82 'strong',
83 array(),
84 Linker::link( $this->getTitle(), $this->getTitle()->getText() )
85 );
86
87 $arr = array(
88 'Page' => array(
89 'type' => 'info',
90 'raw' => true,
91 'default' => $infoText,
92 ),
93 'Reason' => array(
94 'type' => 'selectandother',
95 'label-message' => 'deletecomment',
96 'options-message' => 'deletereason-dropdown',
97 'size' => '60',
98 'maxlength' => '255',
99 'default' => self::getAutoReason( $this->page),
100 ),
101 );
102
103 if( $this->getUser()->isLoggedIn() ){
104 $arr['Watch'] = array(
105 'type' => 'check',
106 'label-message' => 'watchthis',
107 'default' => $this->getUser()->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching()
108 );
109 }
110
111 if( $this->getUser()->isAllowed( 'suppressrevision' ) ){
112 $arr['Suppress'] = array(
113 'type' => 'check',
114 'label-message' => 'revdelete-suppress',
115 'default' => false,
116 );
117 }
118
119 return $arr;
120 }
121
122 /**
123 * Text to go at the top of the form, before the opening fieldset
124 * @see Action::preText()
125 * @return String
126 */
127 protected function preText() {
128
129 // If the page has a history, insert a warning
130 if ( $this->page->estimateRevisionCount() ) {
131 global $wgLang;
132
133 $link = Linker::link(
134 $this->getTitle(),
135 wfMsgHtml( 'history' ),
136 array( 'rel' => 'archives' ),
137 array( 'action' => 'history' )
138 );
139
140 return Html::rawElement(
141 'strong',
142 array( 'class' => 'mw-delete-warning-revisions' ),
143 wfMessage(
144 'historywarning',
145 $wgLang->formatNum( $this->page->estimateRevisionCount() )
146 )->rawParams( $link )->parse()
147 );
148 }
149 }
150
151 /**
152 * Text to go at the bottom of the form, below the closing fieldset
153 * @see Action::postText()
154 * @return string
155 */
156 protected function postText(){
157 $s = '';
158 LogEventsList::showLogExtract(
159 $s,
160 'delete',
161 $this->getTitle()->getPrefixedText()
162 );
163 return Html::element( 'h2', array(), LogPage::logName( 'delete' ) ) . $s;
164 }
165
166 protected function alterForm( HTMLForm &$form ){
167 $form->setWrapperLegend( wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) );
168
169 if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
170 $link = Linker::link(
171 Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ),
172 wfMsgHtml( 'delete-edit-reasonlist' ),
173 array(),
174 array( 'action' => 'edit' )
175 );
176 $form->addHeaderText( '<p class="mw-delete-editreasons">' . $link . '</p>' );
177 }
178 }
179
180 /**
181 * Function called on form submission. Privilege checks and validation have already been
182 * completed by this point; we just need to jump out to the heavy-lifting function,
183 * which is implemented as a static method so it can be called from other places
184 * TODO: make those other places call $action->execute() properly
185 * @see Action::onSubmit()
186 * @param $data Array
187 * @return Array|Bool
188 */
189 public function onSubmit( $data ){
190 $status = self::doDeleteArticle( $this->page, $this->getContext(), $data, true );
191 return $status;
192 }
193
194 public function onSuccess(){
195 // Watch or unwatch, if requested
196 if( $this->getRequest()->getCheck( 'wpWatch' ) && $this->getUser()->isLoggedIn() ) {
197 Action::factory( 'watch', $this->page )->execute();
198 } elseif ( $this->getTitle()->userIsWatching() ) {
199 Action::factory( 'unwatch', $this->page )->execute();
200 }
201
202 $this->getOutput()->setPagetitle( wfMsg( 'actioncomplete' ) );
203 $this->getOutput()->addWikiMsg(
204 'deletedtext',
205 $this->getTitle()->getPrefixedText(),
206 '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'
207 );
208 $this->getOutput()->returnToMain( false );
209 }
210
211 /**
212 * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
213 */
214 protected function isBigDeletion() {
215 global $wgDeleteRevisionsLimit;
216 return $wgDeleteRevisionsLimit && $this->page->estimateRevisionCount() > $wgDeleteRevisionsLimit;
217 }
218
219 /**
220 * Back-end article deletion
221 * Deletes the article with database consistency, writes logs, purges caches
222 *
223 * @param $commit boolean defaults to true, triggers transaction end
224 * @return Bool|Array true if successful, error array on failure
225 */
226 public static function doDeleteArticle( Article $page, RequestContext $context, array $data, $commit = true ) {
227 global $wgDeferredUpdateList, $wgUseTrackbacks;
228
229 wfDebug( __METHOD__ . "\n" );
230
231 // The normal syntax from HTMLSelectAndOtherField is for the reason to be in the form
232 // 'Reason' => array( <full reason>, <dropdown>, <custom> ), but it's reasonable for other
233 // functions to just pass 'Reason' => <reason>
234 $data['Reason'] = (array)$data['Reason'];
235
236 $error = null;
237 if ( !wfRunHooks( 'ArticleDelete', array( &$page, &$context->user, &$data['Reason'][0], &$error ) ) ) {
238 return $error;
239 }
240
241 $title = $page->getTitle();
242 $id = $page->getID( Title::GAID_FOR_UPDATE );
243
244 if ( $title->getDBkey() === '' || $id == 0 ) {
245 return false;
246 }
247
248 $updates = new SiteStatsUpdate( 0, 1, - (int)$page->isCountable( $page->getRawText() ), -1 );
249 array_push( $wgDeferredUpdateList, $updates );
250
251 // Bitfields to further suppress the content
252 if ( isset( $data['Suppress'] ) && $data['Suppress'] ) {
253 $bitfield = 0;
254 // This should be 15...
255 $bitfield |= Revision::DELETED_TEXT;
256 $bitfield |= Revision::DELETED_COMMENT;
257 $bitfield |= Revision::DELETED_USER;
258 $bitfield |= Revision::DELETED_RESTRICTED;
259
260 $logtype = 'suppress';
261 } else {
262 // Otherwise, leave it unchanged
263 $bitfield = 'rev_deleted';
264 $logtype = 'delete';
265 }
266
267 $dbw = wfGetDB( DB_MASTER );
268 $dbw->begin();
269 // For now, shunt the revision data into the archive table.
270 // Text is *not* removed from the text table; bulk storage
271 // is left intact to avoid breaking block-compression or
272 // immutable storage schemes.
273 //
274 // For backwards compatibility, note that some older archive
275 // table entries will have ar_text and ar_flags fields still.
276 //
277 // In the future, we may keep revisions and mark them with
278 // the rev_deleted field, which is reserved for this purpose.
279 $dbw->insertSelect(
280 'archive',
281 array( 'page', 'revision' ),
282 array(
283 'ar_namespace' => 'page_namespace',
284 'ar_title' => 'page_title',
285 'ar_comment' => 'rev_comment',
286 'ar_user' => 'rev_user',
287 'ar_user_text' => 'rev_user_text',
288 'ar_timestamp' => 'rev_timestamp',
289 'ar_minor_edit' => 'rev_minor_edit',
290 'ar_rev_id' => 'rev_id',
291 'ar_text_id' => 'rev_text_id',
292 'ar_text' => "''", // Be explicit to appease
293 'ar_flags' => "''", // MySQL's "strict mode"...
294 'ar_len' => 'rev_len',
295 'ar_page_id' => 'page_id',
296 'ar_deleted' => $bitfield
297 ),
298 array(
299 'page_id' => $id,
300 'page_id = rev_page'
301 ),
302 __METHOD__
303 );
304
305 // Delete restrictions for it
306 $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
307
308 // Now that it's safely backed up, delete it
309 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
310
311 // getArticleId() uses slave, could be laggy
312 if ( $dbw->affectedRows() == 0 ) {
313 $dbw->rollback();
314 return false;
315 }
316
317 // Fix category table counts
318 $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
319 $cats = array();
320 foreach ( $res as $row ) {
321 $cats[] = $row->cl_to;
322 }
323 $page->updateCategoryCounts( array(), $cats );
324
325 // If using cascading deletes, we can skip some explicit deletes
326 if ( !$dbw->cascadingDeletes() ) {
327 $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
328
329 if ( $wgUseTrackbacks ){
330 $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
331 }
332
333 // Delete outgoing links
334 $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
335 $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
336 $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
337 $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
338 $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
339 $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
340 $dbw->delete( 'redirect', array( 'rd_from' => $id ) );
341 }
342
343 // If using cleanup triggers, we can skip some manual deletes
344 if ( !$dbw->cleanupTriggers() ) {
345 // Clean up recentchanges entries...
346 $dbw->delete( 'recentchanges',
347 array(
348 'rc_type != ' . RC_LOG,
349 'rc_namespace' => $title->getNamespace(),
350 'rc_title' => $title->getDBkey() ),
351 __METHOD__
352 );
353 $dbw->delete(
354 'recentchanges',
355 array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
356 __METHOD__
357 );
358 }
359
360 // Clear caches
361 // TODO: should this be in here or left in Article?
362 Article::onArticleDelete( $title );
363
364 // Clear the cached article id so the interface doesn't act like we exist
365 $title->resetArticleID( 0 );
366
367 // Log the deletion, if the page was suppressed, log it at Oversight instead
368 $log = new LogPage( $logtype );
369
370 // Make sure logging got through
371 $log->addEntry( 'delete', $title, $data['Reason'][0], array() );
372
373 if ( $commit ) {
374 $dbw->commit();
375 }
376
377 wfRunHooks( 'ArticleDeleteComplete', array( &$page, &$context->user, $data['Reason'][0], $id ) );
378 return true;
379 }
380
381 /**
382 * Auto-generates a deletion reason. Also sets $this->hasHistory if the page has old
383 * revisions.
384 *
385 * @return mixed String containing default reason or empty string, or boolean false
386 * if no revision was found
387 */
388 public static function getAutoReason( Article $page ) {
389 global $wgContLang;
390
391 $dbw = wfGetDB( DB_MASTER );
392 // Get the last revision
393 $rev = Revision::newFromTitle( $page->getTitle() );
394
395 if ( is_null( $rev ) ) {
396 return false;
397 }
398
399 // Get the article's contents
400 $contents = $rev->getText();
401 $blank = false;
402
403 // If the page is blank, use the text from the previous revision,
404 // which can only be blank if there's a move/import/protect dummy revision involved
405 if ( $contents == '' ) {
406 $prev = $rev->getPrevious();
407
408 if ( $prev ) {
409 $contents = $prev->getText();
410 $blank = true;
411 }
412 }
413
414 // Find out if there was only one contributor
415 // Only scan the last 20 revisions
416 $res = $dbw->select( 'revision', 'rev_user_text',
417 array(
418 'rev_page' => $page->getID(),
419 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
420 ),
421 __METHOD__,
422 array( 'LIMIT' => 20 )
423 );
424
425 if ( $res === false ) {
426 // This page has no revisions, which is very weird
427 return false;
428 }
429
430 $row = $dbw->fetchObject( $res );
431
432 if ( $row ) { // $row is false if the only contributor is hidden
433 $onlyAuthor = $row->rev_user_text;
434 // Try to find a second contributor
435 foreach ( $res as $row ) {
436 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
437 $onlyAuthor = false;
438 break;
439 }
440 }
441 } else {
442 $onlyAuthor = false;
443 }
444
445 // Generate the summary with a '$1' placeholder
446 if ( $blank ) {
447 // The current revision is blank and the one before is also
448 // blank. It's just not our lucky day
449 $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
450 } else {
451 if ( $onlyAuthor ) {
452 $reason = wfMessage( 'excontentauthor', '$1', $onlyAuthor )->inContentLanguage()->text();
453 } else {
454 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
455 }
456 }
457
458 if ( $reason == '-' ) {
459 // Allow these UI messages to be blanked out cleanly
460 return '';
461 }
462
463 // Replace newlines with spaces to prevent uglyness
464 $contents = preg_replace( "/[\n\r]/", ' ', $contents );
465 // Calculate the maximum number of chars to get
466 // Max content length = max comment length - length of the comment (excl. $1)
467 $maxLength = 255 - ( strlen( $reason ) - 2 );
468 $contents = $wgContLang->truncate( $contents, $maxLength );
469 // Remove possible unfinished links
470 $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
471 // Now replace the '$1' placeholder
472 $reason = str_replace( '$1', $contents, $reason );
473
474 return $reason;
475 }
476 }