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