MediaWiki  1.27.2
ProtectionForm.php
Go to the documentation of this file.
1 <?php
31  protected $mRestrictions = [];
32 
34  protected $mReason = '';
35 
37  protected $mReasonSelection = '';
38 
40  protected $mCascade = false;
41 
43  protected $mExpiry = [];
44 
49  protected $mExpirySelection = [];
50 
52  protected $mPermErrors = [];
53 
55  protected $mApplicableTypes = [];
56 
58  protected $mExistingExpiry = [];
59 
61  private $mContext;
62 
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[] = [ 'readonlytext', wfReadOnlyReason() ];
77  }
78  $this->disabled = $this->mPermErrors != [];
79  $this->disabledAttrib = $this->disabled
80  ? [ 'disabled' => 'disabled' ]
81  : [];
82 
83  $this->loadData();
84  }
85 
89  function loadData() {
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 
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 ( wfIsInfinity( $value ) ) {
160  $time = 'infinity';
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 
178  function execute() {
179  if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === [ '' ] ) {
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 
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() === [] ) {
210  // No restriction types available for the current title
211  // this might happen if an extension alters the available types
212  $out->setPageTitle( $this->mContext->msg(
213  'protect-norestrictiontypes-title',
214  $this->mTitle->getPrefixedText()
215  ) );
216  $out->addWikiText( $this->mContext->msg( 'protect-norestrictiontypes-text' )->plain() );
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 
233  $out->wrapWikiMsg(
234  "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>",
235  [ '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  $this->mContext->msg( 'protect-title-notallowed',
244  $this->mTitle->getPrefixedText() )
245  );
246  $out->addWikiText( $out->formatPermissionsErrorMessage( $this->mPermErrors, 'protect' ) );
247  } else {
248  $out->setPageTitle( $this->mContext->msg( '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 
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, [ 'protect', $this->mTitle->getPrefixedDBkey() ] ) ) {
274  $this->show( [ '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 .= $this->mContext->msg( 'colon-separator' )->text() . $this->mReason;
283  } elseif ( $reasonstr == 'other' ) {
284  $reasonstr = $this->mReason;
285  }
286  $expiry = [];
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( [ 'protect_expiry_invalid' ] );
294  return false;
295  }
296  if ( $expiry[$action] < wfTimestampNow() ) {
297  $this->show( [ '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 
323  $errorMsg = '';
324  if ( !Hooks::run( 'ProtectionForm::save', [ $this->mArticle, &$errorMsg, $reasonstr ] ) ) {
325  if ( $errorMsg == '' ) {
326  $errorMsg = [ '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 
344  function buildForm() {
346  $user = $context->getUser();
347  $output = $context->getOutput();
348  $lang = $context->getLanguage();
349  $cascadingRestrictionLevels = $context->getConfig()->get( 'CascadingRestrictionLevels' );
350  $out = '';
351  if ( !$this->disabled ) {
352  $output->addModules( 'mediawiki.legacy.protect' );
353  $output->addJsConfigVars( 'wgCascadeableLevels', $cascadingRestrictionLevels );
354  $out .= Xml::openElement( 'form', [ 'method' => 'post',
355  'action' => $this->mTitle->getLocalURL( 'action=protect' ),
356  'id' => 'mw-Protect-Form' ] );
357  }
358 
359  $out .= Xml::openElement( 'fieldset' ) .
360  Xml::element( 'legend', null, $context->msg( 'protect-legend' )->text() ) .
361  Xml::openElement( 'table', [ 'id' => 'mwProtectSet' ] ) .
362  Xml::openElement( 'tbody' );
363 
364  $scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text();
365  $showProtectOptions = $scExpiryOptions !== '-' && !$this->disabled;
366 
367  // Not all languages have V_x <-> N_x relation
368  foreach ( $this->mRestrictions as $action => $selected ) {
369  // Messages:
370  // restriction-edit, restriction-move, restriction-create, restriction-upload
371  $msg = $context->msg( 'restriction-' . $action );
372  $out .= "<tr><td>" .
373  Xml::openElement( 'fieldset' ) .
374  Xml::element( 'legend', null, $msg->exists() ? $msg->text() : $action ) .
375  Xml::openElement( 'table', [ 'id' => "mw-protect-table-$action" ] ) .
376  "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>";
377 
378  $mProtectexpiry = Xml::label(
379  $context->msg( 'protectexpiry' )->text(),
380  "mwProtectExpirySelection-$action"
381  );
382  $mProtectother = Xml::label(
383  $context->msg( 'protect-othertime' )->text(),
384  "mwProtect-$action-expires"
385  );
386 
387  $expiryFormOptions = new XmlSelect(
388  "wpProtectExpirySelection-$action",
389  "mwProtectExpirySelection-$action",
390  $this->mExpirySelection[$action]
391  );
392  $expiryFormOptions->setAttribute( 'tabindex', '2' );
393  if ( $this->disabled ) {
394  $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
395  }
396 
397  if ( $this->mExistingExpiry[$action] ) {
398  if ( $this->mExistingExpiry[$action] == 'infinity' ) {
399  $existingExpiryMessage = $context->msg( 'protect-existing-expiry-infinity' );
400  } else {
401  $timestamp = $lang->userTimeAndDate( $this->mExistingExpiry[$action], $user );
402  $d = $lang->userDate( $this->mExistingExpiry[$action], $user );
403  $t = $lang->userTime( $this->mExistingExpiry[$action], $user );
404  $existingExpiryMessage = $context->msg(
405  'protect-existing-expiry',
406  $timestamp,
407  $d,
408  $t
409  );
410  }
411  $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
412  }
413 
414  $expiryFormOptions->addOption(
415  $context->msg( 'protect-othertime-op' )->text(),
416  'othertime'
417  );
418  foreach ( explode( ',', $scExpiryOptions ) as $option ) {
419  if ( strpos( $option, ":" ) === false ) {
420  $show = $value = $option;
421  } else {
422  list( $show, $value ) = explode( ":", $option );
423  }
424  $expiryFormOptions->addOption( $show, htmlspecialchars( $value ) );
425  }
426  # Add expiry dropdown
427  if ( $showProtectOptions && !$this->disabled ) {
428  $out .= "
429  <table><tr>
430  <td class='mw-label'>
431  {$mProtectexpiry}
432  </td>
433  <td class='mw-input'>" .
434  $expiryFormOptions->getHTML() .
435  "</td>
436  </tr></table>";
437  }
438  # Add custom expiry field
439  $attribs = [ 'id' => "mwProtect-$action-expires" ] + $this->disabledAttrib;
440  $out .= "<table><tr>
441  <td class='mw-label'>" .
442  $mProtectother .
443  '</td>
444  <td class="mw-input">' .
445  Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) .
446  '</td>
447  </tr></table>';
448  $out .= "</td></tr>" .
449  Xml::closeElement( 'table' ) .
450  Xml::closeElement( 'fieldset' ) .
451  "</td></tr>";
452  }
453  # Give extensions a chance to add items to the form
454  Hooks::run( 'ProtectionForm::buildForm', [ $this->mArticle, &$out ] );
455 
456  $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
457 
458  // JavaScript will add another row with a value-chaining checkbox
459  if ( $this->mTitle->exists() ) {
460  $out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table2' ] ) .
461  Xml::openElement( 'tbody' );
462  $out .= '<tr>
463  <td></td>
464  <td class="mw-input">' .
466  $context->msg( 'protect-cascade' )->text(),
467  'mwProtect-cascade',
468  'mwProtect-cascade',
469  $this->mCascade, $this->disabledAttrib
470  ) .
471  "</td>
472  </tr>\n";
473  $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
474  }
475 
476  # Add manual and custom reason field/selects as well as submit
477  if ( !$this->disabled ) {
478  $mProtectreasonother = Xml::label(
479  $context->msg( 'protectcomment' )->text(),
480  'wpProtectReasonSelection'
481  );
482 
483  $mProtectreason = Xml::label(
484  $context->msg( 'protect-otherreason' )->text(),
485  'mwProtect-reason'
486  );
487 
488  $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
489  wfMessage( 'protect-dropdown' )->inContentLanguage()->text(),
490  wfMessage( 'protect-otherreason-op' )->inContentLanguage()->text(),
491  $this->mReasonSelection,
492  'mwProtect-reason', 4 );
493 
494  $out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table3' ] ) .
495  Xml::openElement( 'tbody' );
496  $out .= "
497  <tr>
498  <td class='mw-label'>
499  {$mProtectreasonother}
500  </td>
501  <td class='mw-input'>
502  {$reasonDropDown}
503  </td>
504  </tr>
505  <tr>
506  <td class='mw-label'>
507  {$mProtectreason}
508  </td>
509  <td class='mw-input'>" .
510  Xml::input( 'mwProtect-reason', 60, $this->mReason, [ 'type' => 'text',
511  'id' => 'mwProtect-reason', 'maxlength' => 180 ] ) .
512  // Limited maxlength as the database trims at 255 bytes and other texts
513  // chosen by dropdown menus on this page are also included in this database field.
514  // The byte limit of 180 bytes is enforced in javascript
515  "</td>
516  </tr>";
517  # Disallow watching is user is not logged in
518  if ( $user->isLoggedIn() ) {
519  $out .= "
520  <tr>
521  <td></td>
522  <td class='mw-input'>" .
523  Xml::checkLabel( $context->msg( 'watchthis' )->text(),
524  'mwProtectWatch', 'mwProtectWatch',
525  $user->isWatched( $this->mTitle ) || $user->getOption( 'watchdefault' ) ) .
526  "</td>
527  </tr>";
528  }
529  $out .= "
530  <tr>
531  <td></td>
532  <td class='mw-submit'>" .
534  $context->msg( 'confirm' )->text(),
535  [ 'id' => 'mw-Protect-submit' ]
536  ) .
537  "</td>
538  </tr>\n";
539  $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
540  }
541  $out .= Xml::closeElement( 'fieldset' );
542 
543  if ( $user->isAllowed( 'editinterface' ) ) {
545  $context->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(),
546  $context->msg( 'protect-edit-reasonlist' )->escaped(),
547  [],
548  [ 'action' => 'edit' ]
549  );
550  $out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
551  }
552 
553  if ( !$this->disabled ) {
554  $out .= Html::hidden(
555  'wpEditToken',
556  $user->getEditToken( [ 'protect', $this->mTitle->getPrefixedDBkey() ] )
557  );
558  $out .= Xml::closeElement( 'form' );
559  }
560 
561  return $out;
562  }
563 
571  function buildSelector( $action, $selected ) {
572  // If the form is disabled, display all relevant levels. Otherwise,
573  // just show the ones this user can use.
574  $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(),
575  $this->disabled ? null : $this->mContext->getUser()
576  );
577 
578  $id = 'mwProtect-level-' . $action;
579 
580  $select = new XmlSelect( $id, $id, $selected );
581  $select->setAttribute( 'size', count( $levels ) );
582  if ( $this->disabled ) {
583  $select->setAttribute( 'disabled', 'disabled' );
584  }
585 
586  foreach ( $levels as $key ) {
587  $select->addOption( $this->getOptionLabel( $key ), $key );
588  }
589 
590  return $select->getHTML();
591  }
592 
599  private function getOptionLabel( $permission ) {
600  if ( $permission == '' ) {
601  return $this->mContext->msg( 'protect-default' )->text();
602  } else {
603  // Messages: protect-level-autoconfirmed, protect-level-sysop
604  $msg = $this->mContext->msg( "protect-level-{$permission}" );
605  if ( $msg->exists() ) {
606  return $msg->text();
607  }
608  return $this->mContext->msg( 'protect-fallback', $permission )->text();
609  }
610  }
611 
618  function showLogExtract( &$out ) {
619  # Show relevant lines from the protection log:
620  $protectLogPage = new LogPage( 'protect' );
621  $out->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
622  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle );
623  # Let extensions add other relevant log extracts
624  Hooks::run( 'ProtectionForm::showLogExtract', [ $this->mArticle, $out ] );
625  }
626 }
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:762
save()
Save submitted protection form.
static linkKnown($target, $html=null, $customAttribs=[], $query=[], $options=[ 'known', 'noclasses'])
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:264
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
buildForm()
Build the input form.
$context
Definition: load.php:44
static element($element, $attribs=null, $contents= '', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:39
buildSelector($action, $selected)
Build protection level selector.
if(!isset($args[0])) $lang
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:759
Class for generating HTML