Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 227
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stabilization
0.00% covered (danger)
0.00%
0 / 227
0.00% covered (danger)
0.00%
0 / 7
1332
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
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 / 42
0.00% covered (danger)
0.00%
0 / 1
132
 showForm
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 1
182
 buildSelector
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 getOptionLabel
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 disabledAttr
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3use MediaWiki\CommentStore\CommentStore;
4use MediaWiki\Exception\PermissionsError;
5use MediaWiki\Exception\UserBlockedError;
6use MediaWiki\Html\Html;
7use MediaWiki\Linker\Linker;
8use MediaWiki\Logging\LogEventsList;
9use MediaWiki\Logging\LogPage;
10use MediaWiki\Permissions\PermissionManager;
11use MediaWiki\SpecialPage\UnlistedSpecialPage;
12use MediaWiki\Title\Title;
13use MediaWiki\User\Options\UserOptionsLookup;
14use MediaWiki\Watchlist\WatchlistManager;
15use MediaWiki\Xml\XmlSelect;
16
17/** Assumes $wgFlaggedRevsProtection is off */
18class Stabilization extends UnlistedSpecialPage {
19    /** @var PageStabilityGeneralForm|null */
20    private $form = null;
21
22    public function __construct(
23        private readonly PermissionManager $permissionManager,
24        private readonly UserOptionsLookup $userOptionsLookup,
25        private readonly WatchlistManager $watchlistManager,
26    ) {
27        parent::__construct( 'Stabilization', 'stablesettings' );
28    }
29
30    /**
31     * @inheritDoc
32     */
33    public function doesWrites() {
34        return true;
35    }
36
37    /**
38     * @inheritDoc
39     */
40    public function execute( $par ) {
41        $out = $this->getOutput();
42        $user = $this->getUser();
43        $request = $this->getRequest();
44
45        $confirmed = $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
46
47        # Target page
48        $title = Title::newFromText( $request->getVal( 'page', $par ) );
49        if ( !$title ) {
50            $out->showErrorPage( 'notargettitle', 'notargettext' );
51            return;
52        }
53
54        # Let anyone view, but not submit...
55        if ( $request->wasPosted() ) {
56            if ( !$this->permissionManager->userHasRight( $user, 'stablesettings' ) ) {
57                throw new PermissionsError( 'stablesettings' );
58            }
59            if ( $this->permissionManager->isBlockedFrom( $user, $title, !$confirmed ) ) {
60                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Guaranteed via isBlockedFrom() above
61                throw new UserBlockedError( $user->getBlock( !$confirmed ) );
62            }
63            $this->checkReadOnly();
64        }
65
66        # Set page title
67        $this->setHeaders();
68
69        $this->getSkin()->setRelevantTitle( $title );
70
71        $this->form = new PageStabilityGeneralForm( $user );
72        $form = $this->form; // convenience
73
74        $form->setTitle( $title );
75        # Watch checkbox
76        $form->setWatchThis( $request->getCheck( 'wpWatchthis' ) );
77        # Get auto-review option...
78        $form->setReviewThis( $request->getCheck( 'wpReviewthis' ) );
79        # Reason
80        $form->setReasonExtra( $request->getText( 'wpReason' ) );
81        $form->setReasonSelection( $request->getVal( 'wpReasonSelection' ) );
82        # Expiry
83        $form->setExpiryCustom( $request->getText( 'mwStabilize-expiry' ) );
84        $form->setExpirySelection( $request->getVal( 'wpExpirySelection' ) );
85        # Default version
86        $form->setOverride( (int)$request->getBool( 'wpStableconfig-override' ) );
87        # Get autoreview restrictions...
88        $form->setAutoreview( $request->getVal( 'mwProtect-level-autoreview' ) );
89        $form->ready(); // params all set
90
91        $status = $form->checkTarget();
92        if ( $status === 'stabilize_page_notexists' ) {
93            $out->addWikiMsg( 'stabilization-notexists', $title->getPrefixedText() );
94            return;
95        } elseif ( $status === 'stabilize_page_unreviewable' ) {
96            $out->addWikiMsg( 'stabilization-notcontent', $title->getPrefixedText() );
97            return;
98        }
99
100        # Form POST request...
101        if ( $request->wasPosted() && $confirmed && $form->isAllowed() ) {
102            $status = $form->submit();
103            if ( $status === true ) {
104                $out->redirect( $title->getFullURL() );
105            } else {
106                $this->showForm( $this->msg( $status )->escaped() );
107            }
108        # Form GET request...
109        } else {
110            $form->preload();
111            $this->showForm();
112        }
113    }
114
115    /**
116     * @param string|null $err
117     */
118    private function showForm( $err = null ) {
119        $out = $this->getOutput();
120        $user = $this->getUser();
121
122        $form = $this->form; // convenience
123        $title = $this->form->getTitle();
124        $oldConfig = $form->getOldConfig();
125
126        $s = ''; // form HTML string
127        # Add any error messages
128        if ( $err ) {
129            $out->setSubtitle( $this->msg( 'formerror' ) );
130            $out->addHTML( "<p class='error'>{$err}</p>\n" );
131        }
132        # Add header text
133        $msg = $form->isAllowed() ? 'stabilization-text' : 'stabilization-perm';
134        $s .= $this->msg( $msg, $title->getPrefixedText() )->parseAsBlock();
135        # Traditionally, the list of reasons for stabilization is the same as
136        # for protection.  In some cases, however, it might be desirable to
137        # use a different list for stabilization.
138        $defaultReasons = $this->msg( 'stabilization-dropdown' );
139        if ( $defaultReasons->isDisabled() ) {
140            $defaultReasons = $this->msg( 'protect-dropdown' );
141        }
142
143        $reasonDropdown = new XmlSelect( 'wpReasonSelection', 'wpReasonSelection' );
144
145        $dropdownOptions = Html::listDropdownOptions(
146            $defaultReasons->inContentLanguage()->text(),
147            [
148                'other' => $this->msg( 'protect-otherreason-op' )->inContentLanguage()->text()
149            ]
150        );
151        $reasonDropdown->addOptions( $dropdownOptions );
152        $reasonDropdown->setDefault( $form->getReasonSelection() );
153        $reasonDropdown->setAttribute( 'class', 'mwStabilize-reason' );
154
155        $scExpiryOptions = $this->msg( 'protect-expiry-options' )->inContentLanguage()->text();
156        $showProtectOptions = ( $scExpiryOptions !== '-' && $form->isAllowed() );
157        $dropdownOptions = []; // array of <label,value>
158        # Add the current expiry as a dropdown option
159        if ( $oldConfig['expiry'] && $oldConfig['expiry'] != 'infinity' ) {
160            $timestamp = $this->getLanguage()->timeanddate( $oldConfig['expiry'] );
161            $d = $this->getLanguage()->date( $oldConfig['expiry'] );
162            $t = $this->getLanguage()->time( $oldConfig['expiry'] );
163            $dropdownOptions[] = [
164                $this->msg( 'protect-existing-expiry', $timestamp, $d, $t )->text(), 'existing' ];
165        }
166        # Add "other time" expiry dropdown option
167        $dropdownOptions[] = [ $this->msg( 'protect-othertime-op' )->text(), 'othertime' ];
168        # Add custom expiry dropdown options (from MediaWiki message)
169        foreach ( explode( ',', $scExpiryOptions ) as $option ) {
170            $pair = explode( ':', $option, 2 );
171            $show = $pair[0];
172            $value = $pair[1] ?? $show;
173            $dropdownOptions[] = [ $show, $value ];
174        }
175
176        # Actually build the options HTML...
177        $expiryFormOptions = '';
178        foreach ( $dropdownOptions as [ $show, $value ] ) {
179            $expiryFormOptions .= Html::element( 'option',
180                [ 'value' => $value, 'selected' => $form->getExpirySelection() === $value ],
181                $show
182            ) . "\n";
183        }
184
185        # Build up the form...
186        $s .= Html::openElement( 'form', [ 'name' => 'stabilization',
187            'action' => $this->getPageTitle()->getLocalURL(), 'method' => 'post' ] );
188        # Add "Revision displayed on default page view"
189        $s .=
190            Html::openElement( 'fieldset' ) .
191            Html::element( 'legend', [], $this->msg( 'stabilization-def' )->text() ) . "\n" .
192            Html::radio( 'wpStableconfig-override', $form->getOverride() == 1, array_merge(
193                [
194                    'id' => 'default-stable',
195                    'value' => '1',
196                ],
197                $this->disabledAttr()
198            ) ) . '&nbsp;' .
199            Html::label( $this->msg( 'stabilization-def1' )->text(), 'default-stable' ) . '<br>' . "\n" .
200            Html::radio( 'wpStableconfig-override', $form->getOverride() == 0, array_merge(
201                [
202                    'id' => 'default-current',
203                    'value' => '0',
204                ],
205                $this->disabledAttr()
206            ) ) . '&nbsp;' .
207            Html::label( $this->msg( 'stabilization-def2' )->text(), 'default-current' ) . "\n" .
208            Html::closeElement( 'fieldset' );
209        # Add "Review/auto-review restrictions"
210        $s .= Html::openElement( 'fieldset' ) .
211            Html::element( 'legend', [], $this->msg( 'stabilization-restrict' )->text() ) .
212            $this->buildSelector( $form->getAutoreview() ) .
213            Html::closeElement( 'fieldset' );
214        # Add "Confirm stable version settings"
215        $s .= Html::openElement( 'fieldset' ) .
216            Html::element( 'legend', [], $this->msg( 'stabilization-leg' )->text() ) .
217            Html::openElement( 'table' );
218        # Add expiry dropdown to form...
219        if ( $showProtectOptions && $form->isAllowed() ) {
220            $s .= "
221                <tr>
222                    <td class='mw-label'>" .
223                        Html::label( $this->msg( 'stabilization-expiry' )->text(),
224                            'mwStabilizeExpirySelection' ) .
225                    "</td>
226                    <td class='mw-input'>" .
227                        Html::rawElement( 'select',
228                            [
229                                'id'        => 'mwStabilizeExpirySelection',
230                                'name'      => 'wpExpirySelection',
231                                'onchange'  => 'onFRChangeExpiryDropdown()',
232                            ] + $this->disabledAttr(),
233                            $expiryFormOptions ) .
234                    "</td>
235                </tr>";
236        }
237        # Add custom expiry field to form...
238        $attribs = [ 'id' => "mwStabilizeExpiryOther",
239            'size' => 50,
240            'oninput' => 'onFRChangeExpiryField()' ] + $this->disabledAttr();
241        $s .= "
242            <tr>
243                <td class='mw-label'>" .
244                    Html::label( $this->msg( 'stabilization-othertime' )->text(),
245                        'mwStabilizeExpiryOther' ) .
246                '</td>
247                <td class="mw-input">' .
248                    Html::input( 'mwStabilize-expiry', $form->getExpiryCustom(), 'text', $attribs ) .
249                '</td>
250            </tr>';
251        # Add comment input and submit button
252        if ( $form->isAllowed() ) {
253            $watchAttribs = [ 'accesskey' => $this->msg( 'accesskey-watch' )->text(),
254                'id' => 'wpWatchthis' ];
255            $watchChecked = ( $this->userOptionsLookup->getOption( $user, 'watchdefault' )
256                || $this->watchlistManager->isWatched( $user, $title ) );
257
258            $s .= ' <tr>
259                    <td class="mw-label">' .
260                        Html::label( $this->msg( 'stabilization-comment' )->text(),
261                            'wpReasonSelection' ) .
262                    '</td>
263                    <td class="mw-input">' .
264                        $reasonDropdown->getHTML() .
265                    '</td>
266                </tr>
267                <tr>
268                    <td class="mw-label">' .
269                        Html::label( $this->msg( 'stabilization-otherreason' )->text(), 'wpReason' ) .
270                    '</td>
271                    <td class="mw-input">' .
272                        Html::input( 'wpReason', $form->getReasonExtra(), 'text', [
273                            'id' => 'wpReason',
274                            'size' => 70,
275                            'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT
276                        ] ) .
277                    '</td>
278                </tr>
279                <tr>
280                    <td></td>
281                    <td class="mw-input">' .
282                        Html::check( 'wpReviewthis', $form->getReviewThis(),
283                            [ 'id' => 'wpReviewthis' ] ) .
284                        Html::label( $this->msg( 'stabilization-review' )->text(), 'wpReviewthis' ) .
285                        '&#160;&#160;&#160;&#160;&#160;' .
286                        Html::check( 'wpWatchthis', $watchChecked, $watchAttribs ) .
287                        "&#160;" .
288                        Html::label( $this->msg( 'watchthis' )->text(), 'wpWatchthis',
289                            [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
290                    '</td>
291                </tr>
292                <tr>
293                    <td></td>
294                    <td class="mw-submit">' .
295                        Html::submitButton( $this->msg( 'stabilization-submit' )->text() ) .
296                    '</td>
297                </tr>' . Html::closeElement( 'table' ) .
298                Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
299                Html::hidden( 'page', $title->getPrefixedText() ) .
300                Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
301        } else {
302            $s .= Html::closeElement( 'table' );
303        }
304        $s .= Html::closeElement( 'fieldset' ) . Html::closeElement( 'form' );
305
306        $out->addHTML( $s );
307
308        $log = new LogPage( 'stable' );
309        $out->addHTML( Html::element( 'h2', [],
310            $log->getName()->setContext( $this->getContext() )->text() ) );
311        LogEventsList::showLogExtract( $out, 'stable',
312            $title->getPrefixedText(), '', [ 'lim' => 25 ] );
313
314        # Add some javascript for expiry dropdowns
315        $out->addScript(
316            "<script type=\"text/javascript\">
317                function onFRChangeExpiryDropdown() {
318                    document.getElementById('mwStabilizeExpiryOther').value = '';
319                }
320                function onFRChangeExpiryField() {
321                    document.getElementById('mwStabilizeExpirySelection').value = 'othertime';
322                }
323            </script>"
324        );
325    }
326
327    /**
328     * @param string $selected
329     * @return string HTML
330     */
331    private function buildSelector( $selected ) {
332        $allowedLevels = [];
333        // Add a "none" level
334        $levels = [ '', ...FlaggedRevs::getRestrictionLevels() ];
335        foreach ( $levels as $key ) {
336            # Don't let them choose levels they can't set,
337            # but *show* them all when the form is disabled.
338            if ( $this->form->isAllowed()
339                && !FlaggedRevs::userCanSetAutoreviewLevel( $this->getUser(), $key )
340            ) {
341                continue;
342            }
343            $allowedLevels[] = $key;
344        }
345        $id = 'mwProtect-level-autoreview';
346        $attribs = [
347            'id' => $id,
348            'name' => $id,
349            'size' => count( $allowedLevels ),
350        ] + $this->disabledAttr();
351
352        $out = '';
353        foreach ( $allowedLevels as $key ) {
354            $out .= Html::element( 'option',
355                [ 'value' => $key, 'selected' => $key == $selected ],
356                $this->getOptionLabel( $key )
357            );
358        }
359        return Html::rawElement( 'select', $attribs, $out );
360    }
361
362    /**
363     * Prepare the label for a protection selector option
364     *
365     * @param string $permission Permission required
366     * @return string
367     */
368    private function getOptionLabel( $permission ) {
369        if ( !$permission ) {
370            return $this->msg( 'stabilization-restrict-none' )->text();
371        }
372
373        $msg = $this->msg( "protect-level-$permission" );
374        if ( $msg->isDisabled() ) {
375            $msg = $this->msg( 'protect-fallback', $permission );
376        }
377        return $msg->text();
378    }
379
380    /**
381     * If the this form is disabled, then return the "disabled" attr array
382     * @return string[]
383     */
384    private function disabledAttr() {
385        return $this->form->isAllowed()
386            ? []
387            : [ 'disabled' => 'disabled' ];
388    }
389}