[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Page protection 4 * 5 * Copyright © 2005 Brion Vibber <[email protected]> 6 * https://www.mediawiki.org/ 7 * 8 * This program is free software; you can redistribute it and/or modify 9 * it under the terms of the GNU General Public License as published by 10 * the Free Software Foundation; either version 2 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU General Public License for more details. 17 * 18 * You should have received a copy of the GNU General Public License along 19 * with this program; if not, write to the Free Software Foundation, Inc., 20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 21 * http://www.gnu.org/copyleft/gpl.html 22 * 23 * @file 24 */ 25 26 /** 27 * Handles the page protection UI and backend 28 */ 29 class ProtectionForm { 30 /** @var array A map of action to restriction level, from request or default */ 31 protected $mRestrictions = array(); 32 33 /** @var string The custom/additional protection reason */ 34 protected $mReason = ''; 35 36 /** @var string The reason selected from the list, blank for other/additional */ 37 protected $mReasonSelection = ''; 38 39 /** @var bool True if the restrictions are cascading, from request or existing protection */ 40 protected $mCascade = false; 41 42 /** @var array Map of action to "other" expiry time. Used in preference to mExpirySelection. */ 43 protected $mExpiry = array(); 44 45 /** 46 * @var array Map of action to value selected in expiry drop-down list. 47 * Will be set to 'othertime' whenever mExpiry is set. 48 */ 49 protected $mExpirySelection = array(); 50 51 /** @var array Permissions errors for the protect action */ 52 protected $mPermErrors = array(); 53 54 /** @var array Types (i.e. actions) for which levels can be selected */ 55 protected $mApplicableTypes = array(); 56 57 /** @var array Map of action to the expiry time of the existing protection */ 58 protected $mExistingExpiry = array(); 59 60 /** @var IContextSource */ 61 private $mContext; 62 63 function __construct( Article $article ) { 64 // Set instance variables. 65 $this->mArticle = $article; 66 $this->mTitle = $article->getTitle(); 67 $this->mApplicableTypes = $this->mTitle->getRestrictionTypes(); 68 $this->mContext = $article->getContext(); 69 70 // Check if the form should be disabled. 71 // If it is, the form will be available in read-only to show levels. 72 $this->mPermErrors = $this->mTitle->getUserPermissionsErrors( 73 'protect', $this->mContext->getUser() 74 ); 75 if ( wfReadOnly() ) { 76 $this->mPermErrors[] = array( 'readonlytext', wfReadOnlyReason() ); 77 } 78 $this->disabled = $this->mPermErrors != array(); 79 $this->disabledAttrib = $this->disabled 80 ? array( 'disabled' => 'disabled' ) 81 : array(); 82 83 $this->loadData(); 84 } 85 86 /** 87 * Loads the current state of protection into the object. 88 */ 89 function loadData() { 90 $levels = MWNamespace::getRestrictionLevels( 91 $this->mTitle->getNamespace(), $this->mContext->getUser() 92 ); 93 $this->mCascade = $this->mTitle->areRestrictionsCascading(); 94 95 $request = $this->mContext->getRequest(); 96 $this->mReason = $request->getText( 'mwProtect-reason' ); 97 $this->mReasonSelection = $request->getText( 'wpProtectReasonSelection' ); 98 $this->mCascade = $request->getBool( 'mwProtect-cascade', $this->mCascade ); 99 100 foreach ( $this->mApplicableTypes as $action ) { 101 // @todo FIXME: This form currently requires individual selections, 102 // but the db allows multiples separated by commas. 103 104 // Pull the actual restriction from the DB 105 $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); 106 107 if ( !$this->mRestrictions[$action] ) { 108 // No existing expiry 109 $existingExpiry = ''; 110 } else { 111 $existingExpiry = $this->mTitle->getRestrictionExpiry( $action ); 112 } 113 $this->mExistingExpiry[$action] = $existingExpiry; 114 115 $requestExpiry = $request->getText( "mwProtect-expiry-$action" ); 116 $requestExpirySelection = $request->getVal( "wpProtectExpirySelection-$action" ); 117 118 if ( $requestExpiry ) { 119 // Custom expiry takes precedence 120 $this->mExpiry[$action] = $requestExpiry; 121 $this->mExpirySelection[$action] = 'othertime'; 122 } elseif ( $requestExpirySelection ) { 123 // Expiry selected from list 124 $this->mExpiry[$action] = ''; 125 $this->mExpirySelection[$action] = $requestExpirySelection; 126 } elseif ( $existingExpiry ) { 127 // Use existing expiry in its own list item 128 $this->mExpiry[$action] = ''; 129 $this->mExpirySelection[$action] = $existingExpiry; 130 } else { 131 // Catches 'infinity' - Existing expiry is infinite, use "infinite" in drop-down 132 // Final default: infinite 133 $this->mExpiry[$action] = ''; 134 $this->mExpirySelection[$action] = 'infinite'; 135 } 136 137 $val = $request->getVal( "mwProtect-level-$action" ); 138 if ( isset( $val ) && in_array( $val, $levels ) ) { 139 $this->mRestrictions[$action] = $val; 140 } 141 } 142 } 143 144 /** 145 * Get the expiry time for a given action, by combining the relevant inputs. 146 * 147 * @param string $action 148 * 149 * @return string 14-char timestamp or "infinity", or false if the input was invalid 150 */ 151 function getExpiry( $action ) { 152 if ( $this->mExpirySelection[$action] == 'existing' ) { 153 return $this->mExistingExpiry[$action]; 154 } elseif ( $this->mExpirySelection[$action] == 'othertime' ) { 155 $value = $this->mExpiry[$action]; 156 } else { 157 $value = $this->mExpirySelection[$action]; 158 } 159 if ( $value == 'infinite' || $value == 'indefinite' || $value == 'infinity' ) { 160 $time = wfGetDB( DB_SLAVE )->getInfinity(); 161 } else { 162 $unix = strtotime( $value ); 163 164 if ( !$unix || $unix === -1 ) { 165 return false; 166 } 167 168 // @todo FIXME: Non-qualified absolute times are not in users specified timezone 169 // and there isn't notice about it in the ui 170 $time = wfTimestamp( TS_MW, $unix ); 171 } 172 return $time; 173 } 174 175 /** 176 * Main entry point for action=protect and action=unprotect 177 */ 178 function execute() { 179 if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === array( '' ) ) { 180 throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' ); 181 } 182 183 if ( $this->mContext->getRequest()->wasPosted() ) { 184 if ( $this->save() ) { 185 $q = $this->mArticle->isRedirect() ? 'redirect=no' : ''; 186 $this->mContext->getOutput()->redirect( $this->mTitle->getFullURL( $q ) ); 187 } 188 } else { 189 $this->show(); 190 } 191 } 192 193 /** 194 * Show the input form with optional error message 195 * 196 * @param string $err Error message or null if there's no error 197 */ 198 function show( $err = null ) { 199 $out = $this->mContext->getOutput(); 200 $out->setRobotPolicy( 'noindex,nofollow' ); 201 $out->addBacklinkSubtitle( $this->mTitle ); 202 203 if ( is_array( $err ) ) { 204 $out->wrapWikiMsg( "<p class='error'>\n$1\n</p>\n", $err ); 205 } elseif ( is_string( $err ) ) { 206 $out->addHTML( "<p class='error'>{$err}</p>\n" ); 207 } 208 209 if ( $this->mTitle->getRestrictionTypes() === array() ) { 210 // No restriction types available for the current title 211 // this might happen if an extension alters the available types 212 $out->setPageTitle( wfMessage( 213 'protect-norestrictiontypes-title', 214 $this->mTitle->getPrefixedText() 215 ) ); 216 $out->addWikiText( wfMessage( 'protect-norestrictiontypes-text' )->text() ); 217 218 // Show the log in case protection was possible once 219 $this->showLogExtract( $out ); 220 // return as there isn't anything else we can do 221 return; 222 } 223 224 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources(); 225 if ( $cascadeSources && count( $cascadeSources ) > 0 ) { 226 $titles = ''; 227 228 foreach ( $cascadeSources as $title ) { 229 $titles .= '* [[:' . $title->getPrefixedText() . "]]\n"; 230 } 231 232 /** @todo FIXME: i18n issue, should use formatted number. */ 233 $out->wrapWikiMsg( 234 "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>", 235 array( 'protect-cascadeon', count( $cascadeSources ) ) 236 ); 237 } 238 239 # Show an appropriate message if the user isn't allowed or able to change 240 # the protection settings at this time 241 if ( $this->disabled ) { 242 $out->setPageTitle( 243 wfMessage( 'protect-title-notallowed', 244 $this->mTitle->getPrefixedText() ) 245 ); 246 $out->addWikiText( $out->formatPermissionsErrorMessage( $this->mPermErrors, 'protect' ) ); 247 } else { 248 $out->setPageTitle( wfMessage( 'protect-title', $this->mTitle->getPrefixedText() ) ); 249 $out->addWikiMsg( 'protect-text', 250 wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ); 251 } 252 253 $out->addHTML( $this->buildForm() ); 254 $this->showLogExtract( $out ); 255 } 256 257 /** 258 * Save submitted protection form 259 * 260 * @return bool Success 261 */ 262 function save() { 263 # Permission check! 264 if ( $this->disabled ) { 265 $this->show(); 266 return false; 267 } 268 269 $request = $this->mContext->getRequest(); 270 $user = $this->mContext->getUser(); 271 $out = $this->mContext->getOutput(); 272 $token = $request->getVal( 'wpEditToken' ); 273 if ( !$user->matchEditToken( $token, array( 'protect', $this->mTitle->getPrefixedDBkey() ) ) ) { 274 $this->show( array( 'sessionfailure' ) ); 275 return false; 276 } 277 278 # Create reason string. Use list and/or custom string. 279 $reasonstr = $this->mReasonSelection; 280 if ( $reasonstr != 'other' && $this->mReason != '' ) { 281 // Entry from drop down menu + additional comment 282 $reasonstr .= wfMessage( 'colon-separator' )->text() . $this->mReason; 283 } elseif ( $reasonstr == 'other' ) { 284 $reasonstr = $this->mReason; 285 } 286 $expiry = array(); 287 foreach ( $this->mApplicableTypes as $action ) { 288 $expiry[$action] = $this->getExpiry( $action ); 289 if ( empty( $this->mRestrictions[$action] ) ) { 290 continue; // unprotected 291 } 292 if ( !$expiry[$action] ) { 293 $this->show( array( 'protect_expiry_invalid' ) ); 294 return false; 295 } 296 if ( $expiry[$action] < wfTimestampNow() ) { 297 $this->show( array( 'protect_expiry_old' ) ); 298 return false; 299 } 300 } 301 302 $this->mCascade = $request->getBool( 'mwProtect-cascade' ); 303 304 $status = $this->mArticle->doUpdateRestrictions( 305 $this->mRestrictions, 306 $expiry, 307 $this->mCascade, 308 $reasonstr, 309 $user 310 ); 311 312 if ( !$status->isOK() ) { 313 $this->show( $out->parseInline( $status->getWikiText() ) ); 314 return false; 315 } 316 317 /** 318 * Give extensions a change to handle added form items 319 * 320 * @since 1.19 you can (and you should) return false to abort saving; 321 * you can also return an array of message name and its parameters 322 */ 323 $errorMsg = ''; 324 if ( !wfRunHooks( 'ProtectionForm::save', array( $this->mArticle, &$errorMsg, $reasonstr ) ) ) { 325 if ( $errorMsg == '' ) { 326 $errorMsg = array( 'hookaborted' ); 327 } 328 } 329 if ( $errorMsg != '' ) { 330 $this->show( $errorMsg ); 331 return false; 332 } 333 334 WatchAction::doWatchOrUnwatch( $request->getCheck( 'mwProtectWatch' ), $this->mTitle, $user ); 335 336 return true; 337 } 338 339 /** 340 * Build the input form 341 * 342 * @return string HTML form 343 */ 344 function buildForm() { 345 $user = $this->mContext->getUser(); 346 $output = $this->mContext->getOutput(); 347 $lang = $this->mContext->getLanguage(); 348 $cascadingRestrictionLevels = $this->mContext->getConfig()->get( 'CascadingRestrictionLevels' ); 349 $out = ''; 350 if ( !$this->disabled ) { 351 $output->addModules( 'mediawiki.legacy.protect' ); 352 $output->addJsConfigVars( 'wgCascadeableLevels', $cascadingRestrictionLevels ); 353 $out .= Xml::openElement( 'form', array( 'method' => 'post', 354 'action' => $this->mTitle->getLocalURL( 'action=protect' ), 355 'id' => 'mw-Protect-Form' ) ); 356 } 357 358 $out .= Xml::openElement( 'fieldset' ) . 359 Xml::element( 'legend', null, wfMessage( 'protect-legend' )->text() ) . 360 Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) . 361 Xml::openElement( 'tbody' ); 362 363 $scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text(); 364 $showProtectOptions = $scExpiryOptions !== '-' && !$this->disabled; 365 366 // Not all languages have V_x <-> N_x relation 367 foreach ( $this->mRestrictions as $action => $selected ) { 368 // Messages: 369 // restriction-edit, restriction-move, restriction-create, restriction-upload 370 $msg = wfMessage( 'restriction-' . $action ); 371 $out .= "<tr><td>" . 372 Xml::openElement( 'fieldset' ) . 373 Xml::element( 'legend', null, $msg->exists() ? $msg->text() : $action ) . 374 Xml::openElement( 'table', array( 'id' => "mw-protect-table-$action" ) ) . 375 "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>"; 376 377 $mProtectexpiry = Xml::label( 378 wfMessage( 'protectexpiry' )->text(), 379 "mwProtectExpirySelection-$action" 380 ); 381 $mProtectother = Xml::label( 382 wfMessage( 'protect-othertime' )->text(), 383 "mwProtect-$action-expires" 384 ); 385 386 $expiryFormOptions = ''; 387 if ( $this->mExistingExpiry[$action] ) { 388 if ( $this->mExistingExpiry[$action] == 'infinity' ) { 389 $existingExpiryMessage = wfMessage( 'protect-existing-expiry-infinity' ); 390 } else { 391 $timestamp = $lang->timeanddate( $this->mExistingExpiry[$action], true ); 392 $d = $lang->date( $this->mExistingExpiry[$action], true ); 393 $t = $lang->time( $this->mExistingExpiry[$action], true ); 394 $existingExpiryMessage = wfMessage( 'protect-existing-expiry', $timestamp, $d, $t ); 395 } 396 $expiryFormOptions .= 397 Xml::option( 398 $existingExpiryMessage->text(), 399 'existing', 400 $this->mExpirySelection[$action] == 'existing' 401 ) . "\n"; 402 } 403 404 $expiryFormOptions .= Xml::option( 405 wfMessage( 'protect-othertime-op' )->text(), 406 "othertime" 407 ) . "\n"; 408 foreach ( explode( ',', $scExpiryOptions ) as $option ) { 409 if ( strpos( $option, ":" ) === false ) { 410 $show = $value = $option; 411 } else { 412 list( $show, $value ) = explode( ":", $option ); 413 } 414 $show = htmlspecialchars( $show ); 415 $value = htmlspecialchars( $value ); 416 $expiryFormOptions .= Xml::option( 417 $show, 418 $value, 419 $this->mExpirySelection[$action] === $value 420 ) . "\n"; 421 } 422 # Add expiry dropdown 423 if ( $showProtectOptions && !$this->disabled ) { 424 $out .= " 425 <table><tr> 426 <td class='mw-label'> 427 {$mProtectexpiry} 428 </td> 429 <td class='mw-input'>" . 430 Xml::tags( 'select', 431 array( 432 'id' => "mwProtectExpirySelection-$action", 433 'name' => "wpProtectExpirySelection-$action", 434 'tabindex' => '2' ) + $this->disabledAttrib, 435 $expiryFormOptions ) . 436 "</td> 437 </tr></table>"; 438 } 439 # Add custom expiry field 440 $attribs = array( 'id' => "mwProtect-$action-expires" ) + $this->disabledAttrib; 441 $out .= "<table><tr> 442 <td class='mw-label'>" . 443 $mProtectother . 444 '</td> 445 <td class="mw-input">' . 446 Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) . 447 '</td> 448 </tr></table>'; 449 $out .= "</td></tr>" . 450 Xml::closeElement( 'table' ) . 451 Xml::closeElement( 'fieldset' ) . 452 "</td></tr>"; 453 } 454 # Give extensions a chance to add items to the form 455 wfRunHooks( 'ProtectionForm::buildForm', array( $this->mArticle, &$out ) ); 456 457 $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); 458 459 // JavaScript will add another row with a value-chaining checkbox 460 if ( $this->mTitle->exists() ) { 461 $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) . 462 Xml::openElement( 'tbody' ); 463 $out .= '<tr> 464 <td></td> 465 <td class="mw-input">' . 466 Xml::checkLabel( 467 wfMessage( 'protect-cascade' )->text(), 468 'mwProtect-cascade', 469 'mwProtect-cascade', 470 $this->mCascade, $this->disabledAttrib 471 ) . 472 "</td> 473 </tr>\n"; 474 $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); 475 } 476 477 # Add manual and custom reason field/selects as well as submit 478 if ( !$this->disabled ) { 479 $mProtectreasonother = Xml::label( 480 wfMessage( 'protectcomment' )->text(), 481 'wpProtectReasonSelection' 482 ); 483 484 $mProtectreason = Xml::label( 485 wfMessage( 'protect-otherreason' )->text(), 486 'mwProtect-reason' 487 ); 488 489 $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection', 490 wfMessage( 'protect-dropdown' )->inContentLanguage()->text(), 491 wfMessage( 'protect-otherreason-op' )->inContentLanguage()->text(), 492 $this->mReasonSelection, 493 'mwProtect-reason', 4 ); 494 495 $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) . 496 Xml::openElement( 'tbody' ); 497 $out .= " 498 <tr> 499 <td class='mw-label'> 500 {$mProtectreasonother} 501 </td> 502 <td class='mw-input'> 503 {$reasonDropDown} 504 </td> 505 </tr> 506 <tr> 507 <td class='mw-label'> 508 {$mProtectreason} 509 </td> 510 <td class='mw-input'>" . 511 Xml::input( 'mwProtect-reason', 60, $this->mReason, array( 'type' => 'text', 512 'id' => 'mwProtect-reason', 'maxlength' => 180 ) ) . 513 // Limited maxlength as the database trims at 255 bytes and other texts 514 // chosen by dropdown menus on this page are also included in this database field. 515 // The byte limit of 180 bytes is enforced in javascript 516 "</td> 517 </tr>"; 518 # Disallow watching is user is not logged in 519 if ( $user->isLoggedIn() ) { 520 $out .= " 521 <tr> 522 <td></td> 523 <td class='mw-input'>" . 524 Xml::checkLabel( wfMessage( 'watchthis' )->text(), 525 'mwProtectWatch', 'mwProtectWatch', 526 $user->isWatched( $this->mTitle ) || $user->getOption( 'watchdefault' ) ) . 527 "</td> 528 </tr>"; 529 } 530 $out .= " 531 <tr> 532 <td></td> 533 <td class='mw-submit'>" . 534 Xml::submitButton( 535 wfMessage( 'confirm' )->text(), 536 array( 'id' => 'mw-Protect-submit' ) 537 ) . 538 "</td> 539 </tr>\n"; 540 $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); 541 } 542 $out .= Xml::closeElement( 'fieldset' ); 543 544 if ( $user->isAllowed( 'editinterface' ) ) { 545 $title = Title::makeTitle( NS_MEDIAWIKI, 'Protect-dropdown' ); 546 $link = Linker::link( 547 $title, 548 wfMessage( 'protect-edit-reasonlist' )->escaped(), 549 array(), 550 array( 'action' => 'edit' ) 551 ); 552 $out .= '<p class="mw-protect-editreasons">' . $link . '</p>'; 553 } 554 555 if ( !$this->disabled ) { 556 $out .= Html::hidden( 557 'wpEditToken', 558 $user->getEditToken( array( 'protect', $this->mTitle->getPrefixedDBkey() ) ) 559 ); 560 $out .= Xml::closeElement( 'form' ); 561 } 562 563 return $out; 564 } 565 566 /** 567 * Build protection level selector 568 * 569 * @param string $action Action to protect 570 * @param string $selected Current protection level 571 * @return string HTML fragment 572 */ 573 function buildSelector( $action, $selected ) { 574 // If the form is disabled, display all relevant levels. Otherwise, 575 // just show the ones this user can use. 576 $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(), 577 $this->disabled ? null : $this->mContext->getUser() 578 ); 579 580 $id = 'mwProtect-level-' . $action; 581 $attribs = array( 582 'id' => $id, 583 'name' => $id, 584 'size' => count( $levels ), 585 ) + $this->disabledAttrib; 586 587 $out = Xml::openElement( 'select', $attribs ); 588 foreach ( $levels as $key ) { 589 $out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected ); 590 } 591 $out .= Xml::closeElement( 'select' ); 592 return $out; 593 } 594 595 /** 596 * Prepare the label for a protection selector option 597 * 598 * @param string $permission Permission required 599 * @return string 600 */ 601 private function getOptionLabel( $permission ) { 602 if ( $permission == '' ) { 603 return wfMessage( 'protect-default' )->text(); 604 } else { 605 // Messages: protect-level-autoconfirmed, protect-level-sysop 606 $msg = wfMessage( "protect-level-{$permission}" ); 607 if ( $msg->exists() ) { 608 return $msg->text(); 609 } 610 return wfMessage( 'protect-fallback', $permission )->text(); 611 } 612 } 613 614 /** 615 * Show protection long extracts for this page 616 * 617 * @param OutputPage $out 618 * @access private 619 */ 620 function showLogExtract( &$out ) { 621 # Show relevant lines from the protection log: 622 $protectLogPage = new LogPage( 'protect' ); 623 $out->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) ); 624 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle ); 625 # Let extensions add other relevant log extracts 626 wfRunHooks( 'ProtectionForm::showLogExtract', array( $this->mArticle, $out ) ); 627 } 628 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |