Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 227 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
| Stabilization | |
0.00% |
0 / 227 |
|
0.00% |
0 / 7 |
1332 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
132 | |||
| showForm | |
0.00% |
0 / 154 |
|
0.00% |
0 / 1 |
182 | |||
| buildSelector | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
| getOptionLabel | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| disabledAttr | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\CommentStore\CommentStore; |
| 4 | use MediaWiki\Exception\PermissionsError; |
| 5 | use MediaWiki\Exception\UserBlockedError; |
| 6 | use MediaWiki\Html\Html; |
| 7 | use MediaWiki\Linker\Linker; |
| 8 | use MediaWiki\Logging\LogEventsList; |
| 9 | use MediaWiki\Logging\LogPage; |
| 10 | use MediaWiki\Permissions\PermissionManager; |
| 11 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
| 12 | use MediaWiki\Title\Title; |
| 13 | use MediaWiki\User\Options\UserOptionsLookup; |
| 14 | use MediaWiki\Watchlist\WatchlistManager; |
| 15 | use MediaWiki\Xml\XmlSelect; |
| 16 | |
| 17 | /** Assumes $wgFlaggedRevsProtection is off */ |
| 18 | class 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 | ) ) . ' ' . |
| 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 | ) ) . ' ' . |
| 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 | '     ' . |
| 286 | Html::check( 'wpWatchthis', $watchChecked, $watchAttribs ) . |
| 287 | " " . |
| 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 | } |