MediaWiki REL1_35
ProtectionForm.php
Go to the documentation of this file.
1<?php
29
35 protected $mRestrictions = [];
36
38 protected $mReason = '';
39
41 protected $mReasonSelection = '';
42
44 protected $mCascade = false;
45
47 protected $mExpiry = [];
48
53 protected $mExpirySelection = [];
54
56 protected $mPermErrors = [];
57
59 protected $mApplicableTypes = [];
60
62 protected $mExistingExpiry = [];
63
65 protected $mArticle;
66
68 protected $mTitle;
69
71 protected $disabled;
72
74 protected $disabledAttrib;
75
77 private $mContext;
78
80 private $permManager;
81
83 private $hookRunner;
84
85 public function __construct( Article $article ) {
86 // Set instance variables.
87 $this->mArticle = $article;
88 $this->mTitle = $article->getTitle();
89 $this->mApplicableTypes = $this->mTitle->getRestrictionTypes();
90 $this->mContext = $article->getContext();
91
92 $services = MediaWikiServices::getInstance();
93 $this->permManager = $services->getPermissionManager();
94 $this->hookRunner = new HookRunner( $services->getHookContainer() );
95
96 // Check if the form should be disabled.
97 // If it is, the form will be available in read-only to show levels.
98 $this->mPermErrors = $this->permManager->getPermissionErrors(
99 'protect',
100 $this->mContext->getUser(),
101 $this->mTitle,
102 $this->mContext->getRequest()->wasPosted()
103 ? PermissionManager::RIGOR_SECURE
104 : PermissionManager::RIGOR_FULL // T92357
105 );
106 if ( wfReadOnly() ) {
107 $this->mPermErrors[] = [ 'readonlytext', wfReadOnlyReason() ];
108 }
109 $this->disabled = $this->mPermErrors !== [];
110 $this->disabledAttrib = $this->disabled
111 ? [ 'disabled' => 'disabled' ]
112 : [];
113
114 $this->loadData();
115 }
116
120 private function loadData() {
121 $levels = $this->permManager->getNamespaceRestrictionLevels(
122 $this->mTitle->getNamespace(), $this->mContext->getUser()
123 );
124 $this->mCascade = $this->mTitle->areRestrictionsCascading();
125
126 $request = $this->mContext->getRequest();
127 $this->mReason = $request->getText( 'mwProtect-reason' );
128 $this->mReasonSelection = $request->getText( 'wpProtectReasonSelection' );
129 $this->mCascade = $request->getBool( 'mwProtect-cascade', $this->mCascade );
130
131 foreach ( $this->mApplicableTypes as $action ) {
132 // @todo FIXME: This form currently requires individual selections,
133 // but the db allows multiples separated by commas.
134
135 // Pull the actual restriction from the DB
136 $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
137
138 if ( !$this->mRestrictions[$action] ) {
139 // No existing expiry
140 $existingExpiry = '';
141 } else {
142 $existingExpiry = $this->mTitle->getRestrictionExpiry( $action );
143 }
144 $this->mExistingExpiry[$action] = $existingExpiry;
145
146 $requestExpiry = $request->getText( "mwProtect-expiry-$action" );
147 $requestExpirySelection = $request->getVal( "wpProtectExpirySelection-$action" );
148
149 if ( $requestExpiry ) {
150 // Custom expiry takes precedence
151 $this->mExpiry[$action] = $requestExpiry;
152 $this->mExpirySelection[$action] = 'othertime';
153 } elseif ( $requestExpirySelection ) {
154 // Expiry selected from list
155 $this->mExpiry[$action] = '';
156 $this->mExpirySelection[$action] = $requestExpirySelection;
157 } elseif ( $existingExpiry ) {
158 // Use existing expiry in its own list item
159 $this->mExpiry[$action] = '';
160 $this->mExpirySelection[$action] = $existingExpiry;
161 } else {
162 // Catches 'infinity' - Existing expiry is infinite, use "infinite" in drop-down
163 // Final default: infinite
164 $this->mExpiry[$action] = '';
165 $this->mExpirySelection[$action] = 'infinite';
166 }
167
168 $val = $request->getVal( "mwProtect-level-$action" );
169 if ( isset( $val ) && in_array( $val, $levels ) ) {
170 $this->mRestrictions[$action] = $val;
171 }
172 }
173 }
174
182 private function getExpiry( $action ) {
183 if ( $this->mExpirySelection[$action] == 'existing' ) {
184 return $this->mExistingExpiry[$action];
185 } elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
186 $value = $this->mExpiry[$action];
187 } else {
188 $value = $this->mExpirySelection[$action];
189 }
190 if ( wfIsInfinity( $value ) ) {
191 $time = 'infinity';
192 } else {
193 $unix = strtotime( $value );
194
195 if ( !$unix || $unix === -1 ) {
196 return false;
197 }
198
199 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
200 // and there isn't notice about it in the ui
201 $time = wfTimestamp( TS_MW, $unix );
202 }
203 return $time;
204 }
205
209 public function execute() {
210 if (
211 $this->permManager->getNamespaceRestrictionLevels(
212 $this->mTitle->getNamespace()
213 ) === [ '' ]
214 ) {
215 throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
216 }
217
218 if ( $this->mContext->getRequest()->wasPosted() ) {
219 if ( $this->save() ) {
220 $q = $this->mArticle->getPage()->isRedirect() ? 'redirect=no' : '';
221 $this->mContext->getOutput()->redirect( $this->mTitle->getFullURL( $q ) );
222 }
223 } else {
224 $this->show();
225 }
226 }
227
233 private function show( $err = null ) {
234 $out = $this->mContext->getOutput();
235 $out->setRobotPolicy( 'noindex,nofollow' );
236 $out->addBacklinkSubtitle( $this->mTitle );
237
238 if ( is_array( $err ) ) {
239 $out->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", $err );
240 } elseif ( is_string( $err ) ) {
241 $out->addHTML( "<div class='error'>{$err}</div>\n" );
242 }
243
244 if ( $this->mTitle->getRestrictionTypes() === [] ) {
245 // No restriction types available for the current title
246 // this might happen if an extension alters the available types
247 $out->setPageTitle( $this->mContext->msg(
248 'protect-norestrictiontypes-title',
249 $this->mTitle->getPrefixedText()
250 ) );
251 $out->addWikiTextAsInterface(
252 $this->mContext->msg( 'protect-norestrictiontypes-text' )->plain()
253 );
254
255 // Show the log in case protection was possible once
256 $this->showLogExtract( $out );
257 // return as there isn't anything else we can do
258 return;
259 }
260
261 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
262 if ( $cascadeSources && count( $cascadeSources ) > 0 ) {
263 $titles = '';
264
265 foreach ( $cascadeSources as $title ) {
266 $titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
267 }
268
270 $out->wrapWikiMsg(
271 "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>",
272 [ 'protect-cascadeon', count( $cascadeSources ) ]
273 );
274 }
275
276 # Show an appropriate message if the user isn't allowed or able to change
277 # the protection settings at this time
278 if ( $this->disabled ) {
279 $out->setPageTitle(
280 $this->mContext->msg( 'protect-title-notallowed',
281 $this->mTitle->getPrefixedText() )
282 );
283 $out->addWikiTextAsInterface( $out->formatPermissionsErrorMessage(
284 $this->mPermErrors, 'protect'
285 ) );
286 } else {
287 $out->setPageTitle( $this->mContext->msg( 'protect-title', $this->mTitle->getPrefixedText() ) );
288 $out->addWikiMsg( 'protect-text',
289 wfEscapeWikiText( $this->mTitle->getPrefixedText() ) );
290 }
291
292 $out->addHTML( $this->buildForm() );
293 $this->showLogExtract( $out );
294 }
295
301 private function save() {
302 # Permission check!
303 if ( $this->disabled ) {
304 $this->show();
305 return false;
306 }
307
308 $request = $this->mContext->getRequest();
309 $user = $this->mContext->getUser();
310 $out = $this->mContext->getOutput();
311 $token = $request->getVal( 'wpEditToken' );
312 if ( !$user->matchEditToken( $token, [ 'protect', $this->mTitle->getPrefixedDBkey() ] ) ) {
313 $this->show( [ 'sessionfailure' ] );
314 return false;
315 }
316
317 # Create reason string. Use list and/or custom string.
318 $reasonstr = $this->mReasonSelection;
319 if ( $reasonstr != 'other' && $this->mReason != '' ) {
320 // Entry from drop down menu + additional comment
321 $reasonstr .= $this->mContext->msg( 'colon-separator' )->text() . $this->mReason;
322 } elseif ( $reasonstr == 'other' ) {
323 $reasonstr = $this->mReason;
324 }
325 $expiry = [];
326 foreach ( $this->mApplicableTypes as $action ) {
327 $expiry[$action] = $this->getExpiry( $action );
328 if ( empty( $this->mRestrictions[$action] ) ) {
329 // unprotected
330 continue;
331 }
332 if ( !$expiry[$action] ) {
333 $this->show( [ 'protect_expiry_invalid' ] );
334 return false;
335 }
336 if ( $expiry[$action] < wfTimestampNow() ) {
337 $this->show( [ 'protect_expiry_old' ] );
338 return false;
339 }
340 }
341
342 $this->mCascade = $request->getBool( 'mwProtect-cascade' );
343
344 $status = $this->mArticle->getPage()->doUpdateRestrictions(
345 $this->mRestrictions,
346 $expiry,
347 $this->mCascade,
348 $reasonstr,
349 $user
350 );
351
352 if ( !$status->isOK() ) {
353 $this->show( $out->parseInlineAsInterface(
354 $status->getWikiText( false, false, $this->mContext->getLanguage() )
355 ) );
356 return false;
357 }
358
365 $errorMsg = '';
366 if ( !$this->hookRunner->onProtectionForm__save( $this->mArticle, $errorMsg, $reasonstr ) ) {
367 if ( $errorMsg == '' ) {
368 $errorMsg = [ 'hookaborted' ];
369 }
370 }
371 if ( $errorMsg != '' ) {
372 $this->show( $errorMsg );
373 return false;
374 }
375
376 WatchAction::doWatchOrUnwatch( $request->getCheck( 'mwProtectWatch' ), $this->mTitle, $user );
377
378 return true;
379 }
380
386 private function buildForm() {
387 $context = $this->mContext;
388 $user = $context->getUser();
389 $output = $context->getOutput();
390 $lang = $context->getLanguage();
391 $out = '';
392 if ( !$this->disabled ) {
393 $output->addModules( 'mediawiki.legacy.protect' );
394 $out .= Xml::openElement( 'form', [ 'method' => 'post',
395 'action' => $this->mTitle->getLocalURL( 'action=protect' ),
396 'id' => 'mw-Protect-Form' ] );
397 }
398
399 $out .= Xml::openElement( 'fieldset' ) .
400 Xml::element( 'legend', null, $context->msg( 'protect-legend' )->text() ) .
401 Xml::openElement( 'table', [ 'id' => 'mwProtectSet' ] ) .
402 Xml::openElement( 'tbody' );
403
404 $expiryOptionsMsg = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text();
405 $showProtectOptions = $expiryOptionsMsg !== '-' && !$this->disabled;
406 $expiryOptions = XmlSelect::parseOptionsMessage( $expiryOptionsMsg );
407
408 // Not all languages have V_x <-> N_x relation
409 foreach ( $this->mRestrictions as $action => $selected ) {
410 // Messages:
411 // restriction-edit, restriction-move, restriction-create, restriction-upload
412 $msg = $context->msg( 'restriction-' . $action );
413 $out .= "<tr><td>" .
414 Xml::openElement( 'fieldset' ) .
415 Xml::element( 'legend', null, $msg->exists() ? $msg->text() : $action ) .
416 Xml::openElement( 'table', [ 'id' => "mw-protect-table-$action" ] ) .
417 "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>";
418
419 $mProtectexpiry = Xml::label(
420 $context->msg( 'protectexpiry' )->text(),
421 "mwProtectExpirySelection-$action"
422 );
423 $mProtectother = Xml::label(
424 $context->msg( 'protect-othertime' )->text(),
425 "mwProtect-$action-expires"
426 );
427
428 $expiryFormOptions = new XmlSelect(
429 "wpProtectExpirySelection-$action",
430 "mwProtectExpirySelection-$action",
431 $this->mExpirySelection[$action]
432 );
433 $expiryFormOptions->setAttribute( 'tabindex', '2' );
434 if ( $this->disabled ) {
435 $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
436 }
437
438 if ( $this->mExistingExpiry[$action] ) {
439 if ( $this->mExistingExpiry[$action] == 'infinity' ) {
440 $existingExpiryMessage = $context->msg( 'protect-existing-expiry-infinity' );
441 } else {
442 $timestamp = $lang->userTimeAndDate( $this->mExistingExpiry[$action], $user );
443 $d = $lang->userDate( $this->mExistingExpiry[$action], $user );
444 $t = $lang->userTime( $this->mExistingExpiry[$action], $user );
445 $existingExpiryMessage = $context->msg(
446 'protect-existing-expiry',
447 $timestamp,
448 $d,
449 $t
450 );
451 }
452 $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
453 }
454
455 $expiryFormOptions->addOption(
456 $context->msg( 'protect-othertime-op' )->text(),
457 'othertime'
458 );
459 $expiryFormOptions->addOptions( $expiryOptions );
460 # Add expiry dropdown
461 if ( $showProtectOptions && !$this->disabled ) {
462 $out .= "
463 <table><tr>
464 <td class='mw-label'>
465 {$mProtectexpiry}
466 </td>
467 <td class='mw-input'>" .
468 $expiryFormOptions->getHTML() .
469 "</td>
470 </tr></table>";
471 }
472 # Add custom expiry field
473 $attribs = [ 'id' => "mwProtect-$action-expires" ] + $this->disabledAttrib;
474 $out .= "<table><tr>
475 <td class='mw-label'>" .
476 $mProtectother .
477 '</td>
478 <td class="mw-input">' .
479 Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) .
480 '</td>
481 </tr></table>';
482 $out .= "</td></tr>" .
483 Xml::closeElement( 'table' ) .
484 Xml::closeElement( 'fieldset' ) .
485 "</td></tr>";
486 }
487 # Give extensions a chance to add items to the form
488 $this->hookRunner->onProtectionForm__buildForm( $this->mArticle, $out );
489
490 $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
491
492 // JavaScript will add another row with a value-chaining checkbox
493 if ( $this->mTitle->exists() ) {
494 $out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table2' ] ) .
495 Xml::openElement( 'tbody' );
496 $out .= '<tr>
497 <td></td>
498 <td class="mw-input">' .
499 Xml::checkLabel(
500 $context->msg( 'protect-cascade' )->text(),
501 'mwProtect-cascade',
502 'mwProtect-cascade',
503 $this->mCascade, $this->disabledAttrib
504 ) .
505 "</td>
506 </tr>\n";
507 $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
508 }
509
510 # Add manual and custom reason field/selects as well as submit
511 if ( !$this->disabled ) {
512 $mProtectreasonother = Xml::label(
513 $context->msg( 'protectcomment' )->text(),
514 'wpProtectReasonSelection'
515 );
516
517 $mProtectreason = Xml::label(
518 $context->msg( 'protect-otherreason' )->text(),
519 'mwProtect-reason'
520 );
521
522 $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
523 wfMessage( 'protect-dropdown' )->inContentLanguage()->text(),
524 wfMessage( 'protect-otherreason-op' )->inContentLanguage()->text(),
525 $this->mReasonSelection,
526 'mwProtect-reason', 4 );
527
528 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
529 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
530 // Unicode codepoints.
531 // Subtract arbitrary 75 to leave some space for the autogenerated null edit's summary
532 // and other texts chosen by dropdown menus on this page.
533 $maxlength = CommentStore::COMMENT_CHARACTER_LIMIT - 75;
534
535 $out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table3' ] ) .
536 Xml::openElement( 'tbody' );
537 $out .= "
538 <tr>
539 <td class='mw-label'>
540 {$mProtectreasonother}
541 </td>
542 <td class='mw-input'>
543 {$reasonDropDown}
544 </td>
545 </tr>
546 <tr>
547 <td class='mw-label'>
548 {$mProtectreason}
549 </td>
550 <td class='mw-input'>" .
551 Xml::input( 'mwProtect-reason', 60, $this->mReason, [ 'type' => 'text',
552 'id' => 'mwProtect-reason', 'maxlength' => $maxlength ] ) .
553 "</td>
554 </tr>";
555 # Disallow watching is user is not logged in
556 if ( $user->isLoggedIn() ) {
557 $out .= "
558 <tr>
559 <td></td>
560 <td class='mw-input'>" .
561 Xml::checkLabel( $context->msg( 'watchthis' )->text(),
562 'mwProtectWatch', 'mwProtectWatch',
563 $user->isWatched( $this->mTitle ) || $user->getOption( 'watchdefault' ) ) .
564 "</td>
565 </tr>";
566 }
567 $out .= "
568 <tr>
569 <td></td>
570 <td class='mw-submit'>" .
571 Xml::submitButton(
572 $context->msg( 'confirm' )->text(),
573 [ 'id' => 'mw-Protect-submit' ]
574 ) .
575 "</td>
576 </tr>\n";
577 $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
578 }
579 $out .= Xml::closeElement( 'fieldset' );
580
581 if ( $this->permManager->userHasRight( $user, 'editinterface' ) ) {
582 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
583 $link = $linkRenderer->makeKnownLink(
584 $context->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(),
585 $context->msg( 'protect-edit-reasonlist' )->text(),
586 [],
587 [ 'action' => 'edit' ]
588 );
589 $out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
590 }
591
592 if ( !$this->disabled ) {
593 $out .= Html::hidden(
594 'wpEditToken',
595 $user->getEditToken( [ 'protect', $this->mTitle->getPrefixedDBkey() ] )
596 );
597 $out .= Xml::closeElement( 'form' );
598 }
599
600 return $out;
601 }
602
610 private function buildSelector( $action, $selected ) {
611 // If the form is disabled, display all relevant levels. Otherwise,
612 // just show the ones this user can use.
613 $levels = $this->permManager->getNamespaceRestrictionLevels(
614 $this->mTitle->getNamespace(),
615 $this->disabled ? null : $this->mContext->getUser()
616 );
617
618 $id = 'mwProtect-level-' . $action;
619
620 $select = new XmlSelect( $id, $id, $selected );
621 $select->setAttribute( 'size', count( $levels ) );
622 if ( $this->disabled ) {
623 $select->setAttribute( 'disabled', 'disabled' );
624 }
625
626 foreach ( $levels as $key ) {
627 $select->addOption( $this->getOptionLabel( $key ), $key );
628 }
629
630 return $select->getHTML();
631 }
632
639 private function getOptionLabel( $permission ) {
640 if ( $permission == '' ) {
641 return $this->mContext->msg( 'protect-default' )->text();
642 } else {
643 // Messages: protect-level-autoconfirmed, protect-level-sysop
644 $msg = $this->mContext->msg( "protect-level-{$permission}" );
645 if ( $msg->exists() ) {
646 return $msg->text();
647 }
648 return $this->mContext->msg( 'protect-fallback', $permission )->text();
649 }
650 }
651
657 private function showLogExtract( OutputPage $out ) {
658 # Show relevant lines from the protection log:
659 $protectLogPage = new LogPage( 'protect' );
660 $out->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
661 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle );
662 # Let extensions add other relevant log extracts
663 $this->hookRunner->onProtectionForm__showLogExtract( $this->mArticle, $out );
664 }
665}
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Class for viewing MediaWiki article and history.
Definition Article.php:46
getContext()
Gets the context this Article is executed in.
Definition Article.php:2345
getTitle()
Get the title object of the article.
Definition Article.php:255
An error page which can definitely be safely rendered using the OutputPage.
Class to simplify the use of log pages.
Definition LogPage.php:37
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
MediaWikiServices is the service locator for the application scope of MediaWiki.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
This is one of the Core classes and should be read at least once by any new developers.
addHTML( $text)
Append $text to the body HTML.
Handles the page protection UI and backend.
__construct(Article $article)
execute()
Main entry point for action=protect and action=unprotect.
loadData()
Loads the current state of protection into the object.
buildSelector( $action, $selected)
Build protection level selector.
getOptionLabel( $permission)
Prepare the label for a protection selector option.
array $mApplicableTypes
Types (i.e.
array $mRestrictions
A map of action to restriction level, from request or default.
array $mExistingExpiry
Map of action to the expiry time of the existing protection.
string $mReasonSelection
The reason selected from the list, blank for other/additional.
bool $mCascade
True if the restrictions are cascading, from request or existing protection.
array $mExpirySelection
Map of action to value selected in expiry drop-down list.
IContextSource $mContext
buildForm()
Build the input form.
string $mReason
The custom/additional protection reason.
HookRunner $hookRunner
show( $err=null)
Show the input form with optional error message.
array $mExpiry
Map of action to "other" expiry time.
save()
Save submitted protection form.
showLogExtract(OutputPage $out)
Show protection long extracts for this page.
PermissionManager $permManager
array $mPermErrors
Permissions errors for the protect action.
getExpiry( $action)
Get the expiry time for a given action, by combining the relevant inputs.
Represents a title within MediaWiki.
Definition Title.php:42
static doWatchOrUnwatch( $watch, Title $title, User $user, string $expiry=null)
Watch or unwatch a page.
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:26
static parseOptionsMessage(string $msg)
Parse labels and values out of a comma- and colon-separated list of options, such as is used for expi...
Interface for objects which can provide a MediaWiki context on request.
if(!isset( $args[0])) $lang