034c2b881e1c4c076878446032df2c099f8f391a
3 * Performs the delete action on a page
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.
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.
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
23 class DeleteAction
extends Action
{
25 public function getName(){
29 public function getRestriction(){
33 protected function getDescription(){
34 return wfMsg( 'delete-confirm', $this->getTitle()->getPrefixedText() );
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
45 protected function checkCanExecute( User
$user ){
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
54 LogEventsList
::showLogExtract(
57 $this->getTitle()->getPrefixedText()
60 $msg = new Message( 'cannotdelete' );
61 $msg->params( $this->getTitle()->getPrefixedText() ); // This parameter is parsed
62 $msg->rawParams( $log ); // This is not
64 throw new ErrorPageError( 'internalerror', $msg );
67 // Limit deletions of big pages
68 $bigHistory = $this->isBigDeletion();
69 if ( $bigHistory && !$user->isAllowed( 'bigdelete' ) ) {
70 global $wgDeleteRevisionsLimit;
71 throw new ErrorPageError(
74 $this->getContext()->lang
->formatNum( $wgDeleteRevisionsLimit )
78 return parent
::checkCanExecute( $user );
81 protected function getFormFields(){
82 // TODO: add more useful things here?
83 $infoText = Html
::rawElement(
86 Linker
::link( $this->getTitle(), $this->getTitle()->getText() )
93 'default' => $infoText,
96 'type' => 'selectandother',
97 'label-message' => 'deletecomment',
98 'options-message' => 'deletereason-dropdown',
100 'maxlength' => '255',
101 'default' => self
::getAutoReason( $this->page
),
105 if( $this->getUser()->isLoggedIn() ){
106 $arr['Watch'] = array(
108 'label-message' => 'watchthis',
109 'default' => $this->getUser()->getBoolOption( 'watchdeletion' ) ||
$this->getTitle()->userIsWatching()
113 if( $this->getUser()->isAllowed( 'suppressrevision' ) ){
114 $arr['Suppress'] = array(
116 'label-message' => 'revdelete-suppress',
125 * Text to go at the top of the form, before the opening fieldset
126 * @see Action::preText()
129 protected function preText() {
131 // If the page has a history, insert a warning
132 if ( $this->page
->estimateRevisionCount() ) {
135 $link = Linker
::link(
137 wfMsgHtml( 'history' ),
138 array( 'rel' => 'archives' ),
139 array( 'action' => 'history' )
142 return Html
::rawElement(
144 array( 'class' => 'mw-delete-warning-revisions' ),
147 $wgLang->formatNum( $this->page
->estimateRevisionCount() )
148 )->rawParams( $link )->parse()
154 * Text to go at the bottom of the form, below the closing fieldset
155 * @see Action::postText()
158 protected function postText(){
160 LogEventsList
::showLogExtract(
163 $this->getTitle()->getPrefixedText()
165 return Html
::element( 'h2', array(), LogPage
::logName( 'delete' ) ) . $s;
168 protected function alterForm( HTMLForm
&$form ){
169 $form->setWrapperLegend( wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) );
171 if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
172 $link = Linker
::link(
173 Title
::makeTitle( NS_MEDIAWIKI
, 'Deletereason-dropdown' ),
174 wfMsgHtml( 'delete-edit-reasonlist' ),
176 array( 'action' => 'edit' )
178 $form->addHeaderText( '<p class="mw-delete-editreasons">' . $link . '</p>' );
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()
191 public function onSubmit( $data ){
192 $status = self
::doDeleteArticle( $this->page
, $this->getContext(), $data, true );
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() );
204 $this->getOutput()->setPagetitle( wfMsg( 'actioncomplete' ) );
205 $this->getOutput()->addWikiMsg(
207 wfEscapeWikiText( $this->getTitle()->getPrefixedText() ),
208 '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'
210 $this->getOutput()->returnToMain( false );
214 * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
216 protected function isBigDeletion() {
217 global $wgDeleteRevisionsLimit;
218 return $wgDeleteRevisionsLimit && $this->page
->estimateRevisionCount() > $wgDeleteRevisionsLimit;
222 * Back-end article deletion
223 * Deletes the article with database consistency, writes logs, purges caches
225 * @param $commit boolean defaults to true, triggers transaction end
226 * @return Bool|Array true if successful, error array on failure
228 public static function doDeleteArticle( Article
$page, RequestContext
$context, array $data, $commit = true ) {
229 global $wgDeferredUpdateList, $wgUseTrackbacks;
231 wfDebug( __METHOD__
. "\n" );
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'];
239 if ( !wfRunHooks( 'ArticleDelete', array( &$page, $context->getUser(), &$data['Reason'][0], &$error ) ) ) {
243 $title = $page->getTitle();
244 $id = $page->getID( Title
::GAID_FOR_UPDATE
);
246 if ( $title->getDBkey() === '' ||
$id == 0 ) {
250 $updates = new SiteStatsUpdate( 0, 1, - (int)$page->isCountable(), -1 );
251 array_push( $wgDeferredUpdateList, $updates );
253 // Bitfields to further suppress the content
254 if ( isset( $data['Suppress'] ) && $data['Suppress'] ) {
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
;
262 $logtype = 'suppress';
264 // Otherwise, leave it unchanged
265 $bitfield = 'rev_deleted';
269 $dbw = wfGetDB( DB_MASTER
);
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.
276 // For backwards compatibility, note that some older archive
277 // table entries will have ar_text and ar_flags fields still.
279 // In the future, we may keep revisions and mark them with
280 // the rev_deleted field, which is reserved for this purpose.
283 array( 'page', 'revision' ),
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
307 // Delete restrictions for it
308 $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__
);
310 // Now that it's safely backed up, delete it
311 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__
);
313 // getArticleId() uses slave, could be laggy
314 if ( $dbw->affectedRows() == 0 ) {
319 // Fix category table counts
320 $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__
);
322 foreach ( $res as $row ) {
323 $cats[] = $row->cl_to
;
325 $page->updateCategoryCounts( array(), $cats );
327 // If using cascading deletes, we can skip some explicit deletes
328 if ( !$dbw->cascadingDeletes() ) {
329 $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__
);
331 if ( $wgUseTrackbacks ){
332 $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__
);
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 ) );
345 // If using cleanup triggers, we can skip some manual deletes
346 if ( !$dbw->cleanupTriggers() ) {
347 // Clean up recentchanges entries...
348 $dbw->delete( 'recentchanges',
350 'rc_type != ' . RC_LOG
,
351 'rc_namespace' => $title->getNamespace(),
352 'rc_title' => $title->getDBkey() ),
357 array( 'rc_type != ' . RC_LOG
, 'rc_cur_id' => $id ),
363 // TODO: should this be in here or left in Article?
364 Article
::onArticleDelete( $title );
366 // Clear the cached article id so the interface doesn't act like we exist
367 $title->resetArticleID( 0 );
369 // Log the deletion, if the page was suppressed, log it at Oversight instead
370 $log = new LogPage( $logtype );
372 // Make sure logging got through
373 $log->addEntry( 'delete', $title, $data['Reason'][0], array() );
379 wfRunHooks( 'ArticleDeleteComplete', array( &$page, $context->getUser(), $data['Reason'][0], $id ) );
384 * Auto-generates a deletion reason. Also sets $this->hasHistory if the page has old
387 * @return mixed String containing default reason or empty string, or boolean false
388 * if no revision was found
390 public static function getAutoReason( Article
$page ) {
393 $dbw = wfGetDB( DB_MASTER
);
394 // Get the last revision
395 $rev = Revision
::newFromTitle( $page->getTitle() );
397 if ( is_null( $rev ) ) {
401 // Get the article's contents
402 $contents = $rev->getText();
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();
411 $contents = $prev->getText();
416 // Find out if there was only one contributor
417 // Only scan the last 20 revisions
418 $res = $dbw->select( 'revision', 'rev_user_text',
420 'rev_page' => $page->getID(),
421 $dbw->bitAnd( 'rev_deleted', Revision
::DELETED_USER
) . ' = 0'
424 array( 'LIMIT' => 20 )
427 if ( $res === false ) {
428 // This page has no revisions, which is very weird
432 $row = $dbw->fetchObject( $res );
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
447 // Generate the summary with a '$1' placeholder
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();
454 $reason = wfMessage( 'excontentauthor', '$1', $onlyAuthor )->inContentLanguage()->text();
456 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
460 if ( $reason == '-' ) {
461 // Allow these UI messages to be blanked out cleanly
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 );
479 public function show() {
482 public function execute(){