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