Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 367
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialRevisionDelete
0.00% covered (danger)
0.00%
0 / 366
0.00% covered (danger)
0.00%
0 / 16
5256
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
420
 showConvenienceLinks
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 getLogQueryCond
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 tryShowFile
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
 getList
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 showForm
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 1
110
 addUsageText
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 buildCheckBoxes
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
56
 submit
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 success
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 failure
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 extractBitParams
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 save
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\CommentStore\CommentStore;
10use MediaWiki\Exception\ErrorPageError;
11use MediaWiki\Exception\PermissionsError;
12use MediaWiki\Exception\UserBlockedError;
13use MediaWiki\FileRepo\File\File;
14use MediaWiki\FileRepo\RepoGroup;
15use MediaWiki\Html\Html;
16use MediaWiki\HTMLForm\HTMLForm;
17use MediaWiki\Logging\LogEventsList;
18use MediaWiki\Logging\LogPage;
19use MediaWiki\Permissions\PermissionManager;
20use MediaWiki\Revision\RevisionRecord;
21use MediaWiki\RevisionDelete\RevDelList;
22use MediaWiki\RevisionDelete\RevisionDeleter;
23use MediaWiki\SpecialPage\SpecialPage;
24use MediaWiki\SpecialPage\UnlistedSpecialPage;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\Title;
27
28/**
29 * Special page allowing users with the appropriate permissions to view
30 * and hide revisions. Log items can also be hidden.
31 *
32 * @ingroup SpecialPage
33 */
34class SpecialRevisionDelete extends UnlistedSpecialPage {
35    /** @var bool Was the DB modified in this request */
36    protected $wasSaved = false;
37
38    /** @var bool True if the submit button was clicked, and the form was posted */
39    private $submitClicked;
40
41    /** @var array Target ID list */
42    private $ids;
43
44    /** @var string Archive name, for reviewing deleted files */
45    private $archiveName;
46
47    /** @var string Edit token for securing image views against XSS */
48    private $token;
49
50    /** @var Title Title object for target parameter */
51    private $targetObj;
52
53    /** @var string Deletion type, may be revision, archive, oldimage, filearchive, logging. */
54    private $typeName;
55
56    /** @var array Array of checkbox specs (message, name, deletion bits) */
57    private $checks;
58
59    /** @var array UI Labels about the current type */
60    private $typeLabels;
61
62    /** @var RevDelList RevDelList object, storing the list of items to be deleted/undeleted */
63    private $revDelList;
64
65    /** @var bool Whether user is allowed to perform the action */
66    private $mIsAllowed;
67
68    /** @var string */
69    private $otherReason;
70
71    private PermissionManager $permissionManager;
72    private RepoGroup $repoGroup;
73
74    /**
75     * UI labels for each type.
76     */
77    private const UI_LABELS = [
78        'revision' => [
79            'check-label' => 'revdelete-hide-text',
80            'success' => 'revdelete-success',
81            'failure' => 'revdelete-failure',
82            'text' => 'revdelete-text-text',
83            'selected' => 'revdelete-selected-text',
84        ],
85        'archive' => [
86            'check-label' => 'revdelete-hide-text',
87            'success' => 'revdelete-success',
88            'failure' => 'revdelete-failure',
89            'text' => 'revdelete-text-text',
90            'selected' => 'revdelete-selected-text',
91        ],
92        'oldimage' => [
93            'check-label' => 'revdelete-hide-image',
94            'success' => 'revdelete-success',
95            'failure' => 'revdelete-failure',
96            'text' => 'revdelete-text-file',
97            'selected' => 'revdelete-selected-file',
98        ],
99        'filearchive' => [
100            'check-label' => 'revdelete-hide-image',
101            'success' => 'revdelete-success',
102            'failure' => 'revdelete-failure',
103            'text' => 'revdelete-text-file',
104            'selected' => 'revdelete-selected-file',
105        ],
106        'logging' => [
107            'check-label' => 'revdelete-hide-name',
108            'success' => 'logdelete-success',
109            'failure' => 'logdelete-failure',
110            'text' => 'logdelete-text',
111            'selected' => 'logdelete-selected',
112        ],
113    ];
114
115    public function __construct( PermissionManager $permissionManager, RepoGroup $repoGroup ) {
116        parent::__construct( 'Revisiondelete' );
117
118        $this->permissionManager = $permissionManager;
119        $this->repoGroup = $repoGroup;
120    }
121
122    /** @inheritDoc */
123    public function doesWrites() {
124        return true;
125    }
126
127    /** @inheritDoc */
128    public function execute( $par ) {
129        $this->useTransactionalTimeLimit();
130
131        $this->checkPermissions();
132        $this->checkReadOnly();
133
134        $output = $this->getOutput();
135        $user = $this->getUser();
136
137        $this->setHeaders();
138        $this->outputHeader();
139        $request = $this->getRequest();
140        $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
141        # Handle our many different possible input types.
142        $ids = $request->getVal( 'ids' );
143        if ( $ids !== null ) {
144            # Allow CSV, for backwards compatibility, or a single ID for show/hide links
145            $this->ids = explode( ',', $ids );
146        } else {
147            # Array input
148            $this->ids = array_keys( $request->getArray( 'ids', [] ) );
149        }
150        // $this->ids = array_map( 'intval', $this->ids );
151        $this->ids = array_unique( array_filter( $this->ids ) );
152
153        $this->typeName = $request->getVal( 'type' ) ?? '';
154        $this->targetObj = Title::newFromText( $request->getText( 'target' ) );
155
156        # For reviewing deleted files...
157        $this->archiveName = $request->getVal( 'file' );
158        $this->token = $request->getVal( 'token' );
159        if ( $this->archiveName && $this->targetObj ) {
160            $this->tryShowFile( $this->archiveName );
161
162            return;
163        }
164
165        $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName );
166
167        # No targets?
168        if ( !$this->typeName || count( $this->ids ) == 0 ) {
169            throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
170        }
171
172        $restriction = RevisionDeleter::getRestriction( $this->typeName );
173
174        if ( !$this->getAuthority()->isAllowedAny( $restriction, 'deletedhistory' ) ) {
175            throw new PermissionsError( $restriction );
176        }
177
178        # Allow the list type to adjust the passed target
179        $this->targetObj = RevisionDeleter::suggestTarget(
180            $this->typeName,
181            $this->targetObj,
182            $this->ids
183        );
184
185        # We need a target page!
186        if ( $this->targetObj === null ||
187            ( $this->typeName !== 'logging' && !$this->targetObj->canExist() )
188        ) {
189            $output->addWikiMsg( 'undelete-header' );
190
191            return;
192        }
193
194        // Check blocks
195        $checkReplica = !$this->submitClicked;
196        if (
197            $this->permissionManager->isBlockedFrom(
198                $user,
199                $this->targetObj,
200                $checkReplica
201            )
202        ) {
203            throw new UserBlockedError(
204                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
205                $user->getBlock(),
206                $user,
207                $this->getLanguage(),
208                $request->getIP()
209            );
210        }
211
212        $this->typeLabels = self::UI_LABELS[$this->typeName];
213        $list = $this->getList();
214        $list->reset();
215        $this->mIsAllowed = $this->permissionManager->userHasRight( $user, $restriction );
216        $canViewSuppressedOnly = $this->permissionManager->userHasRight( $user, 'viewsuppressed' ) &&
217            !$this->permissionManager->userHasRight( $user, 'suppressrevision' );
218        $pageIsSuppressed = $list->areAnySuppressed();
219        $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed );
220
221        $this->otherReason = $request->getVal( 'wpReason', '' );
222        # Give a link to the logs/hist for this page
223        $this->showConvenienceLinks();
224
225        # Initialise checkboxes
226        $this->checks = [
227            # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name
228            [ $this->typeLabels['check-label'], 'wpHidePrimary',
229                RevisionDeleter::getRevdelConstant( $this->typeName )
230            ],
231            [ 'revdelete-hide-comment', 'wpHideComment', RevisionRecord::DELETED_COMMENT ],
232            [ 'revdelete-hide-user', 'wpHideUser', RevisionRecord::DELETED_USER ]
233        ];
234        if ( $this->permissionManager->userHasRight( $user, 'suppressrevision' ) ) {
235            $this->checks[] = [ 'revdelete-hide-restricted',
236                'wpHideRestricted', RevisionRecord::DELETED_RESTRICTED ];
237        }
238
239        # Either submit or create our form
240        if ( $this->mIsAllowed && $this->submitClicked ) {
241            $this->submit();
242        } else {
243            $this->showForm();
244        }
245
246        if ( $this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
247            # Show relevant lines from the deletion log
248            $deleteLogPage = new LogPage( 'delete' );
249            $output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" );
250            LogEventsList::showLogExtract(
251                $output,
252                'delete',
253                $this->targetObj,
254                '', /* user */
255                [ 'lim' => 25, 'conds' => $this->getLogQueryCond(), 'useMaster' => $this->wasSaved ]
256            );
257        }
258        # Show relevant lines from the suppression log
259        if ( $this->permissionManager->userHasRight( $user, 'suppressionlog' ) ) {
260            $suppressLogPage = new LogPage( 'suppress' );
261            $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" );
262            LogEventsList::showLogExtract(
263                $output,
264                'suppress',
265                $this->targetObj,
266                '',
267                [ 'lim' => 25, 'conds' => $this->getLogQueryCond(), 'useMaster' => $this->wasSaved ]
268            );
269        }
270    }
271
272    /**
273     * Show some useful links in the subtitle
274     */
275    protected function showConvenienceLinks() {
276        $linkRenderer = $this->getLinkRenderer();
277        # Give a link to the logs/hist for this page
278        if ( $this->targetObj ) {
279            // Also set header tabs to be for the target.
280            $this->getSkin()->setRelevantTitle( $this->targetObj );
281
282            $links = [];
283            $links[] = $linkRenderer->makeKnownLink(
284                SpecialPage::getTitleFor( 'Log' ),
285                $this->msg( 'viewpagelogs' )->text(),
286                [],
287                [ 'page' => $this->targetObj->getPrefixedText() ]
288            );
289            if ( !$this->targetObj->isSpecialPage() ) {
290                # Give a link to the page history
291                $links[] = $linkRenderer->makeKnownLink(
292                    $this->targetObj,
293                    $this->msg( 'pagehist' )->text(),
294                    [],
295                    [ 'action' => 'history' ]
296                );
297                # Link to deleted edits
298                if ( $this->permissionManager->userHasRight( $this->getUser(), 'undelete' ) ) {
299                    $undelete = SpecialPage::getTitleFor( 'Undelete' );
300                    $links[] = $linkRenderer->makeKnownLink(
301                        $undelete,
302                        $this->msg( 'deletedhist' )->text(),
303                        [],
304                        [ 'target' => $this->targetObj->getPrefixedDBkey() ]
305                    );
306                }
307            }
308            # Logs themselves don't have histories or archived revisions
309            $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
310        }
311    }
312
313    /**
314     * Get the condition used for fetching log snippets
315     * @return array
316     */
317    protected function getLogQueryCond() {
318        $conds = [];
319        // Revision delete logs for these item
320        $conds['log_type'] = [ 'delete', 'suppress' ];
321        $conds['log_action'] = $this->getList()->getLogAction();
322        $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName );
323        // Convert IDs to strings, since ls_value is a text field. This avoids
324        // a fatal error in PostgreSQL: "operator does not exist: text = integer".
325        $conds['ls_value'] = array_map( 'strval', $this->ids );
326
327        return $conds;
328    }
329
330    /**
331     * Show a deleted file version requested by the visitor.
332     * @todo Mostly copied from Special:Undelete. Refactor.
333     * @param string $archiveName
334     */
335    protected function tryShowFile( $archiveName ) {
336        if ( $this->targetObj->getNamespace() !== NS_FILE ) {
337            $this->getOutput()->addWikiMsg( 'revdelete-no-file' );
338
339            return;
340        }
341        $repo = $this->repoGroup->getLocalRepo();
342        $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName );
343        $oimage->load();
344        // Check if user is allowed to see this file
345        if ( !$oimage->exists() ) {
346            $this->getOutput()->addWikiMsg( 'revdelete-no-file' );
347
348            return;
349        }
350        $user = $this->getUser();
351        if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
352            if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
353                throw new PermissionsError( 'suppressrevision' );
354            } else {
355                throw new PermissionsError( 'deletedtext' );
356            }
357        }
358        if ( !$user->matchEditToken( $this->token, $archiveName ) ) {
359            $lang = $this->getLanguage();
360            $this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm',
361                $this->targetObj->getText(),
362                $lang->userDate( $oimage->getTimestamp(), $user ),
363                $lang->userTime( $oimage->getTimestamp(), $user ) );
364            $this->getOutput()->addHTML(
365                Html::rawElement( 'form', [
366                    'method' => 'POST',
367                    'action' => $this->getPageTitle()->getLocalURL( [
368                            'target' => $this->targetObj->getPrefixedDBkey(),
369                            'file' => $archiveName,
370                            'token' => $user->getEditToken( $archiveName ),
371                        ] )
372                    ],
373                    Html::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() )
374                )
375            );
376
377            return;
378        }
379        $this->getOutput()->disable();
380        # We mustn't allow the output to be CDN cached, otherwise
381        # if an admin previews a deleted image, and it's cached, then
382        # a user without appropriate permissions can toddle off and
383        # nab the image, and CDN will serve it
384        $this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
385        $this->getRequest()->response()->header(
386            'Cache-Control: no-cache, no-store, max-age=0, must-revalidate'
387        );
388
389        $key = $oimage->getStorageKey();
390        $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
391        $repo->streamFileWithStatus( $path );
392    }
393
394    /**
395     * Get the list object for this request
396     * @return RevDelList
397     */
398    protected function getList() {
399        if ( $this->revDelList === null ) {
400            $this->revDelList = RevisionDeleter::createList(
401                $this->typeName, $this->getContext(), $this->targetObj, $this->ids
402            );
403        }
404
405        return $this->revDelList;
406    }
407
408    /**
409     * Show a list of items that we will operate on, and show a form with checkboxes
410     * which will allow the user to choose new visibility settings.
411     */
412    protected function showForm() {
413        $userAllowed = true;
414
415        // Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected
416        $out = $this->getOutput();
417        $out->wrapWikiMsg( "<strong>$1</strong>", [ $this->typeLabels['selected'],
418            $this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ] );
419
420        $this->addHelpLink( 'Help:RevisionDelete' );
421        $out->addHTML( "<ul>" );
422
423        $numRevisions = 0;
424        // Live revisions...
425        $list = $this->getList();
426        foreach ( $list as $item ) {
427            if ( !$item->canView() ) {
428                if ( !$this->submitClicked ) {
429                    throw new PermissionsError( 'suppressrevision' );
430                }
431                $userAllowed = false;
432            }
433
434            $numRevisions++;
435            $out->addHTML( $item->getHTML() );
436        }
437
438        if ( !$numRevisions ) {
439            throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
440        }
441
442        $out->addHTML( "</ul>" );
443        // Explanation text
444        $this->addUsageText();
445
446        // Normal sysops can always see what they did, but can't always change it
447        if ( !$userAllowed ) {
448            return;
449        }
450
451        // Show form if the user can submit
452        if ( $this->mIsAllowed ) {
453            $suppressAllowed = $this->permissionManager
454                ->userHasRight( $this->getUser(), 'suppressrevision' );
455            $out->addModules( [ 'mediawiki.misc-authed-ooui' ] );
456            $out->addModuleStyles( [ 'mediawiki.special',
457                'mediawiki.interface.helpers.styles' ] );
458
459            $dropdownReason = $this->msg( 'revdelete-reason-dropdown' )
460                ->page( $this->targetObj )->inContentLanguage()->text();
461            // Add additional specific reasons for suppress
462            if ( $suppressAllowed ) {
463                $dropdownReason .= "\n" . $this->msg( 'revdelete-reason-dropdown-suppress' )
464                    ->page( $this->targetObj )->inContentLanguage()->text();
465            }
466
467            $fields = $this->buildCheckBoxes();
468
469            $fields[] = [
470                'type' => 'select',
471                'label' => $this->msg( 'revdelete-log' )->text(),
472                'cssclass' => 'wpReasonDropDown',
473                'id' => 'wpRevDeleteReasonList',
474                'name' => 'wpRevDeleteReasonList',
475                'options' => Html::listDropdownOptions(
476                    $dropdownReason,
477                    [ 'other' => $this->msg( 'revdelete-reasonotherlist' )->text() ]
478                ),
479                'default' => $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' )
480            ];
481
482            $fields[] = [
483                'type' => 'text',
484                'label' => $this->msg( 'revdelete-otherreason' )->text(),
485                'name' => 'wpReason',
486                'id' => 'wpReason',
487                // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
488                // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
489                // Unicode codepoints.
490                // "- 155" is to leave room for the 'wpRevDeleteReasonList' value.
491                'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
492            ];
493
494            $fields[] = [
495                'type' => 'hidden',
496                'name' => 'wpEditToken',
497                'default' => $this->getUser()->getEditToken()
498            ];
499
500            $fields[] = [
501                'type' => 'hidden',
502                'name' => 'target',
503                'default' => $this->targetObj->getPrefixedText()
504            ];
505
506            $fields[] = [
507                'type' => 'hidden',
508                'name' => 'type',
509                'default' => $this->typeName
510            ];
511
512            $fields[] = [
513                'type' => 'hidden',
514                'name' => 'ids',
515                'default' => implode( ',', $this->ids )
516            ];
517
518            $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
519            $htmlForm
520                ->setSubmitText( $this->msg( 'revdelete-submit', $numRevisions )->text() )
521                ->setSubmitName( 'wpSubmit' )
522                ->setWrapperLegend( $this->msg( 'revdelete-legend' )->text() )
523                ->setAction( $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) )
524                ->loadData();
525            // Show link to edit the dropdown reasons
526            if ( $this->permissionManager->userHasRight( $this->getUser(), 'editinterface' ) ) {
527                $link = '';
528                $linkRenderer = $this->getLinkRenderer();
529                if ( $suppressAllowed ) {
530                    $link .= $linkRenderer->makeKnownLink(
531                        $this->msg( 'revdelete-reason-dropdown-suppress' )->inContentLanguage()->getTitle(),
532                        $this->msg( 'revdelete-edit-reasonlist-suppress' )->text(),
533                        [],
534                        [ 'action' => 'edit' ]
535                    );
536                    $link .= $this->msg( 'pipe-separator' )->escaped();
537                }
538                $link .= $linkRenderer->makeKnownLink(
539                    $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->getTitle(),
540                    $this->msg( 'revdelete-edit-reasonlist' )->text(),
541                    [],
542                    [ 'action' => 'edit' ]
543                );
544                $htmlForm->setPostHtml( Html::rawElement( 'p', [ 'class' => 'mw-revdel-editreasons' ], $link ) );
545            }
546            $out->addHTML( $htmlForm->getHTML( false ) );
547        }
548    }
549
550    /**
551     * Show some introductory text
552     * @todo FIXME: Wikimedia-specific policy text
553     */
554    protected function addUsageText() {
555        // Messages: revdelete-text-text, revdelete-text-file, logdelete-text
556        $this->getOutput()->wrapWikiMsg(
557            "<strong>$1</strong>\n$2", $this->typeLabels['text'],
558            'revdelete-text-others'
559        );
560
561        if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) ) {
562            $this->getOutput()->addWikiMsg( 'revdelete-suppress-text' );
563        }
564
565        if ( $this->mIsAllowed ) {
566            $this->getOutput()->addWikiMsg( 'revdelete-confirm' );
567        }
568    }
569
570    /**
571     * @return array $fields
572     */
573    protected function buildCheckBoxes() {
574        $fields = [];
575
576        $type = 'radio';
577
578        $list = $this->getList();
579
580        // If there is just one item, use checkboxes
581        if ( $list->length() == 1 ) {
582            $list->reset();
583
584            $type = 'check';
585        }
586
587        foreach ( $this->checks as $item ) {
588            // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name,
589            // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted
590            [ $message, $name, $bitField ] = $item;
591
592            $field = [
593                'type' => $type,
594                'label-raw' => $this->msg( $message )->escaped(),
595                'id' => $name,
596                'flatlist' => true,
597                'name' => $name,
598                'default' => $list->length() == 1 ? $list->current()->getBits() & $bitField : null
599            ];
600
601            if ( $bitField == RevisionRecord::DELETED_RESTRICTED ) {
602                $field['label-raw'] = "<b>" . $field['label-raw'] . "</b>";
603                if ( $type === 'radio' ) {
604                    $field['options-messages'] = [
605                        'revdelete-radio-same' => -1,
606                        'revdelete-radio-unset-suppress' => 0,
607                        'revdelete-radio-set-suppress' => 1
608                    ];
609                }
610            } elseif ( $type === 'radio' ) {
611                $field['options-messages'] = [
612                    'revdelete-radio-same' => -1,
613                    'revdelete-radio-unset' => 0,
614                    'revdelete-radio-set' => 1
615                ];
616            }
617
618            $fields[] = $field;
619        }
620
621        return $fields;
622    }
623
624    /**
625     * UI entry point for form submission.
626     * @throws PermissionsError
627     * @return bool
628     */
629    protected function submit() {
630        # Check edit token on submission
631        $token = $this->getRequest()->getVal( 'wpEditToken' );
632        if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
633            $this->getOutput()->addWikiMsg( 'sessionfailure' );
634
635            return false;
636        }
637        $bitParams = $this->extractBitParams();
638        // from dropdown
639        $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' );
640        $comment = $listReason;
641        if ( $comment === 'other' ) {
642            $comment = $this->otherReason;
643        } elseif ( $this->otherReason !== '' ) {
644            // Entry from drop down menu + additional comment
645            $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text()
646                . $this->otherReason;
647        }
648        # Can the user set this field?
649        if ( $bitParams[RevisionRecord::DELETED_RESTRICTED] == 1
650            && !$this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' )
651        ) {
652            throw new PermissionsError( 'suppressrevision' );
653        }
654        # If the save went through, go to success message...
655        $status = $this->save( $bitParams, $comment );
656        if ( $status->isGood() ) {
657            $this->success();
658
659            return true;
660        } else {
661            # ...otherwise, bounce back to form...
662            $this->failure( $status );
663        }
664
665        return false;
666    }
667
668    /**
669     * Report that the submit operation succeeded
670     */
671    protected function success() {
672        // Messages: revdelete-success, logdelete-success
673        $out = $this->getOutput();
674        $out->setPageTitleMsg( $this->msg( 'actioncomplete' ) );
675        $out->addHTML(
676            Html::successBox(
677                $out->msg( $this->typeLabels['success'] )->parse()
678            )
679        );
680        $this->wasSaved = true;
681        $this->revDelList->reloadFromPrimary();
682        $this->showForm();
683    }
684
685    /**
686     * Report that the submit operation failed
687     * @param Status $status
688     */
689    protected function failure( $status ) {
690        // Messages: revdelete-failure, logdelete-failure
691        $out = $this->getOutput();
692        $out->setPageTitleMsg( $this->msg( 'actionfailed' ) );
693        $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
694        $out->addHTML(
695            Html::errorBox(
696                $out->parseAsContent(
697                    $status->getWikiText( $this->typeLabels['failure'], false, $this->getLanguage() )
698                )
699            )
700        );
701        $this->showForm();
702    }
703
704    /**
705     * Put together an array that contains -1, 0, or the *_deleted const for each bit
706     *
707     * @return array
708     */
709    protected function extractBitParams() {
710        $bitfield = [];
711        foreach ( $this->checks as [ /* message */, $name, $field ] ) {
712            $val = $this->getRequest()->getInt( $name, 0 /* unchecked */ );
713            if ( $val < -1 || $val > 1 ) {
714                $val = -1; // -1 for existing value
715            }
716            $bitfield[$field] = $val;
717        }
718        if ( !isset( $bitfield[RevisionRecord::DELETED_RESTRICTED] ) ) {
719            $bitfield[RevisionRecord::DELETED_RESTRICTED] = 0;
720        }
721
722        return $bitfield;
723    }
724
725    /**
726     * Do the write operations. Simple wrapper for RevDel*List::setVisibility().
727     * @param array $bitPars ExtractBitParams() bitfield array
728     * @param string $reason
729     * @return Status
730     */
731    protected function save( array $bitPars, $reason ) {
732        return $this->getList()->setVisibility(
733            [ 'value' => $bitPars, 'comment' => $reason ]
734        );
735    }
736
737    /** @inheritDoc */
738    protected function getGroupName() {
739        return 'pagetools';
740    }
741}
742
743/**
744 * Retain the old class name for backwards compatibility.
745 * @deprecated since 1.41
746 */
747class_alias( SpecialRevisionDelete::class, 'SpecialRevisionDelete' );