MediaWiki REL1_37
ProtectionForm.php
Go to the documentation of this file.
1<?php
32
38 protected $mRestrictions = [];
39
41 protected $mReason = '';
42
44 protected $mReasonSelection = '';
45
47 protected $mCascade = false;
48
50 protected $mExpiry = [];
51
56 protected $mExpirySelection = [];
57
59 protected $mPermStatus;
60
62 protected $mApplicableTypes = [];
63
65 protected $mExistingExpiry = [];
66
68 protected $mArticle;
69
71 protected $mTitle;
72
74 protected $disabled;
75
77 protected $disabledAttrib;
78
80 private $mContext;
81
83 private $mRequest;
84
86 private $mPerformer;
87
89 private $mLang;
90
92 private $mOut;
93
95 private $permManager;
96
101
103 private $hookRunner;
104
105 public function __construct( Article $article ) {
106 // Set instance variables.
107 $this->mArticle = $article;
108 $this->mTitle = $article->getTitle();
109 $this->mApplicableTypes = $this->mTitle->getRestrictionTypes();
110 $this->mContext = $article->getContext();
111 $this->mRequest = $this->mContext->getRequest();
112 $this->mPerformer = $this->mContext->getAuthority();
113 $this->mOut = $this->mContext->getOutput();
114 $this->mLang = $this->mContext->getLanguage();
115
116 $services = MediaWikiServices::getInstance();
117 $this->permManager = $services->getPermissionManager();
118 $this->hookRunner = new HookRunner( $services->getHookContainer() );
119 $this->watchlistManager = $services->getWatchlistManager();
120
121 // Check if the form should be disabled.
122 // If it is, the form will be available in read-only to show levels.
123 $this->mPermStatus = PermissionStatus::newEmpty();
124 if ( $this->mRequest->wasPosted() ) {
125 $this->mPerformer->authorizeWrite( 'protect', $this->mTitle, $this->mPermStatus );
126 } else {
127 $this->mPerformer->authorizeRead( 'protect', $this->mTitle, $this->mPermStatus );
128 }
129 if ( wfReadOnly() ) {
130 $this->mPermStatus->fatal( 'readonlytext', wfReadOnlyReason() );
131 }
132 $this->disabled = !$this->mPermStatus->isGood();
133 $this->disabledAttrib = $this->disabled ? [ 'disabled' => 'disabled' ] : [];
134
135 $this->loadData();
136 }
137
141 private function loadData() {
142 $levels = $this->permManager->getNamespaceRestrictionLevels(
143 $this->mTitle->getNamespace(), $this->mPerformer->getUser()
144 );
145
146 $this->mCascade = $this->mTitle->areRestrictionsCascading();
147 $this->mReason = $this->mRequest->getText( 'mwProtect-reason' );
148 $this->mReasonSelection = $this->mRequest->getText( 'wpProtectReasonSelection' );
149 $this->mCascade = $this->mRequest->getBool( 'mwProtect-cascade', $this->mCascade );
150
151 foreach ( $this->mApplicableTypes as $action ) {
152 // @todo FIXME: This form currently requires individual selections,
153 // but the db allows multiples separated by commas.
154
155 // Pull the actual restriction from the DB
156 $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
157
158 if ( !$this->mRestrictions[$action] ) {
159 // No existing expiry
160 $existingExpiry = '';
161 } else {
162 $existingExpiry = $this->mTitle->getRestrictionExpiry( $action );
163 }
164 $this->mExistingExpiry[$action] = $existingExpiry;
165
166 $requestExpiry = $this->mRequest->getText( "mwProtect-expiry-$action" );
167 $requestExpirySelection = $this->mRequest->getVal( "wpProtectExpirySelection-$action" );
168
169 if ( $requestExpiry ) {
170 // Custom expiry takes precedence
171 $this->mExpiry[$action] = $requestExpiry;
172 $this->mExpirySelection[$action] = 'othertime';
173 } elseif ( $requestExpirySelection ) {
174 // Expiry selected from list
175 $this->mExpiry[$action] = '';
176 $this->mExpirySelection[$action] = $requestExpirySelection;
177 } elseif ( $existingExpiry ) {
178 // Use existing expiry in its own list item
179 $this->mExpiry[$action] = '';
180 $this->mExpirySelection[$action] = $existingExpiry;
181 } else {
182 // Catches 'infinity' - Existing expiry is infinite, use "infinite" in drop-down
183 // Final default: infinite
184 $this->mExpiry[$action] = '';
185 $this->mExpirySelection[$action] = 'infinite';
186 }
187
188 $val = $this->mRequest->getVal( "mwProtect-level-$action" );
189 if ( isset( $val ) && in_array( $val, $levels ) ) {
190 $this->mRestrictions[$action] = $val;
191 }
192 }
193 }
194
202 private function getExpiry( $action ) {
203 if ( $this->mExpirySelection[$action] == 'existing' ) {
204 return $this->mExistingExpiry[$action];
205 } elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
206 $value = $this->mExpiry[$action];
207 } else {
208 $value = $this->mExpirySelection[$action];
209 }
210 if ( wfIsInfinity( $value ) ) {
211 $time = 'infinity';
212 } else {
213 $unix = strtotime( $value );
214
215 if ( !$unix || $unix === -1 ) {
216 return false;
217 }
218
219 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
220 // and there isn't notice about it in the ui
221 $time = wfTimestamp( TS_MW, $unix );
222 }
223 return $time;
224 }
225
229 public function execute() {
230 if (
231 $this->permManager->getNamespaceRestrictionLevels(
232 $this->mTitle->getNamespace()
233 ) === [ '' ]
234 ) {
235 throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
236 }
237
238 if ( $this->mRequest->wasPosted() ) {
239 if ( $this->save() ) {
240 $q = $this->mArticle->getPage()->isRedirect() ? 'redirect=no' : '';
241 $this->mOut->redirect( $this->mTitle->getFullURL( $q ) );
242 }
243 } else {
244 $this->show();
245 }
246 }
247
253 private function show( $err = null ) {
254 $out = $this->mOut;
255 $out->setRobotPolicy( 'noindex,nofollow' );
256 $out->addBacklinkSubtitle( $this->mTitle );
257
258 if ( is_array( $err ) ) {
259 $out->addHTML( Html::errorBox( $out->msg( ...$err )->parse() ) );
260 } elseif ( is_string( $err ) ) {
261 $out->addHTML( Html::errorBox( $err ) );
262 }
263
264 if ( $this->mApplicableTypes === [] ) {
265 // No restriction types available for the current title
266 // this might happen if an extension alters the available types
267 $out->setPageTitle( $this->mContext->msg(
268 'protect-norestrictiontypes-title',
269 $this->mTitle->getPrefixedText()
270 ) );
271 $out->addWikiTextAsInterface(
272 $this->mContext->msg( 'protect-norestrictiontypes-text' )->plain()
273 );
274
275 // Show the log in case protection was possible once
276 $this->showLogExtract();
277 // return as there isn't anything else we can do
278 return;
279 }
280
281 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
282 if ( $cascadeSources && count( $cascadeSources ) > 0 ) {
283 $titles = '';
284
285 foreach ( $cascadeSources as $title ) {
286 $titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
287 }
288
290 $out->wrapWikiMsg(
291 "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>",
292 [ 'protect-cascadeon', count( $cascadeSources ) ]
293 );
294 }
295
296 # Show an appropriate message if the user isn't allowed or able to change
297 # the protection settings at this time
298 if ( $this->disabled ) {
299 $out->setPageTitle(
300 $this->mContext->msg( 'protect-title-notallowed',
301 $this->mTitle->getPrefixedText() )
302 );
303 $out->addWikiTextAsInterface(
304 $out->formatPermissionStatus( $this->mPermStatus, 'protect' )
305 );
306 } else {
307 $out->setPageTitle(
308 $this->mContext->msg( 'protect-title', $this->mTitle->getPrefixedText() )
309 );
310 $out->addWikiMsg( 'protect-text',
311 wfEscapeWikiText( $this->mTitle->getPrefixedText() ) );
312 }
313
314 $out->addHTML( $this->buildForm() );
315 $this->showLogExtract();
316 }
317
323 private function save() {
324 # Permission check!
325 if ( $this->disabled ) {
326 $this->show();
327 return false;
328 }
329
330 $token = $this->mRequest->getVal( 'wpEditToken' );
331 $legacyUser = MediaWikiServices::getInstance()
332 ->getUserFactory()
333 ->newFromAuthority( $this->mPerformer );
334 if ( !$legacyUser->matchEditToken( $token, [ 'protect', $this->mTitle->getPrefixedDBkey() ] ) ) {
335 $this->show( [ 'sessionfailure' ] );
336 return false;
337 }
338
339 # Create reason string. Use list and/or custom string.
340 $reasonstr = $this->mReasonSelection;
341 if ( $reasonstr != 'other' && $this->mReason != '' ) {
342 // Entry from drop down menu + additional comment
343 $reasonstr .= $this->mContext->msg( 'colon-separator' )->text() . $this->mReason;
344 } elseif ( $reasonstr == 'other' ) {
345 $reasonstr = $this->mReason;
346 }
347
348 $expiry = [];
349 foreach ( $this->mApplicableTypes as $action ) {
350 $expiry[$action] = $this->getExpiry( $action );
351 if ( empty( $this->mRestrictions[$action] ) ) {
352 // unprotected
353 continue;
354 }
355 if ( !$expiry[$action] ) {
356 $this->show( [ 'protect_expiry_invalid' ] );
357 return false;
358 }
359 if ( $expiry[$action] < wfTimestampNow() ) {
360 $this->show( [ 'protect_expiry_old' ] );
361 return false;
362 }
363 }
364
365 $this->mCascade = $this->mRequest->getBool( 'mwProtect-cascade' );
366
367 $status = $this->mArticle->getPage()->doUpdateRestrictions(
368 $this->mRestrictions,
369 $expiry,
370 $this->mCascade,
371 $reasonstr,
372 $this->mPerformer->getUser()
373 );
374
375 if ( !$status->isOK() ) {
376 $this->show( $this->mOut->parseInlineAsInterface(
377 $status->getWikiText( false, false, $this->mLang )
378 ) );
379 return false;
380 }
381
388 $errorMsg = '';
389 if ( !$this->hookRunner->onProtectionForm__save( $this->mArticle, $errorMsg, $reasonstr ) ) {
390 if ( $errorMsg == '' ) {
391 $errorMsg = [ 'hookaborted' ];
392 }
393 }
394 if ( $errorMsg != '' ) {
395 $this->show( $errorMsg );
396 return false;
397 }
398
399 $this->watchlistManager->setWatch(
400 $this->mRequest->getCheck( 'mwProtectWatch' ),
401 $this->mPerformer,
402 $this->mTitle
403 );
404
405 return true;
406 }
407
413 private function buildForm() {
414 $this->mOut->enableOOUI();
415 $out = '';
416 $fields = [];
417 if ( !$this->disabled ) {
418 $this->mOut->addModules( 'mediawiki.action.protect' );
419 $this->mOut->addModuleStyles( 'mediawiki.action.styles' );
420 }
421 $scExpiryOptions = $this->mContext->msg( 'protect-expiry-options' )->inContentLanguage()->text();
422 $levels = $this->permManager->getNamespaceRestrictionLevels(
423 $this->mTitle->getNamespace(),
424 $this->disabled ? null : $this->mPerformer->getUser()
425 );
426
427 // Not all languages have V_x <-> N_x relation
428 foreach ( $this->mRestrictions as $action => $selected ) {
429 // Messages:
430 // restriction-edit, restriction-move, restriction-create, restriction-upload
431 $section = 'restriction-' . $action;
432 $id = 'mwProtect-level-' . $action;
433 $options = [];
434 foreach ( $levels as $key ) {
435 $options[$this->getOptionLabel( $key )] = $key;
436 }
437
438 $fields[$id] = [
439 'type' => 'select',
440 'name' => $id,
441 'default' => $selected,
442 'id' => $id,
443 'size' => count( $levels ),
444 'options' => $options,
445 'disabled' => $this->disabled,
446 'section' => $section,
447 ];
448
449 $expiryOptions = [];
450
451 if ( $this->mExistingExpiry[$action] ) {
452 if ( $this->mExistingExpiry[$action] == 'infinity' ) {
453 $existingExpiryMessage = $this->mContext->msg( 'protect-existing-expiry-infinity' );
454 } else {
455 $existingExpiryMessage = $this->mContext->msg( 'protect-existing-expiry' )
456 ->dateTimeParams( $this->mExistingExpiry[$action] )
457 ->dateParams( $this->mExistingExpiry[$action] )
458 ->timeParams( $this->mExistingExpiry[$action] );
459 }
460 $expiryOptions[$existingExpiryMessage->text()] = 'existing';
461 }
462
463 $expiryOptions[$this->mContext->msg( 'protect-othertime-op' )->text()] = 'othertime';
464
465 $expiryOptions = array_merge( $expiryOptions, XmlSelect::parseOptionsMessage( $scExpiryOptions ) );
466
467 # Add expiry dropdown
468 $fields["wpProtectExpirySelection-$action"] = [
469 'type' => 'select',
470 'name' => "wpProtectExpirySelection-$action",
471 'id' => "mwProtectExpirySelection-$action",
472 'tabindex' => '2',
473 'disabled' => $this->disabled,
474 'label' => $this->mContext->msg( 'protectexpiry' )->text(),
475 'options' => $expiryOptions,
476 'default' => $this->mExpirySelection[$action],
477 'section' => $section,
478 ];
479
480 # Add custom expiry field
481 if ( !$this->disabled ) {
482 $fields["mwProtect-expiry-$action"] = [
483 'type' => 'text',
484 'label' => $this->mContext->msg( 'protect-othertime' )->text(),
485 'name' => "mwProtect-expiry-$action",
486 'id' => "mwProtect-$action-expires",
487 'size' => 50,
488 'default' => $this->mExpiry[$action],
489 'disabled' => $this->disabled,
490 'section' => $section,
491 ];
492 }
493 }
494
495 # Give extensions a chance to add items to the form
496 $hookFormRaw = '';
497 $hookFormOptions = [];
498
499 $this->hookRunner->onProtectionForm__buildForm( $this->mArticle, $hookFormRaw );
500 $this->hookRunner->onProtectionFormAddFormFields( $this->mArticle, $hookFormOptions );
501
502 # Merge forms added from addFormFields
503 $fields = array_merge( $fields, $hookFormOptions );
504
505 # Add raw sections added in buildForm
506 if ( $hookFormRaw ) {
507 $fields['rawinfo'] = [
508 'type' => 'info',
509 'default' => $hookFormRaw,
510 'raw' => true,
511 'section' => 'restriction-blank'
512 ];
513 }
514
515 # JavaScript will add another row with a value-chaining checkbox
516 if ( $this->mTitle->exists() ) {
517 $fields['mwProtect-cascade'] = [
518 'type' => 'check',
519 'label' => $this->mContext->msg( 'protect-cascade' )->text(),
520 'id' => 'mwProtect-cascade',
521 'name' => 'mwProtect-cascade',
522 'default' => $this->mCascade,
523 'disabled' => $this->disabled,
524 ];
525 }
526
527 # Add manual and custom reason field/selects as well as submit
528 if ( !$this->disabled ) {
529 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
530 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
531 // Unicode codepoints.
532 // Subtract arbitrary 75 to leave some space for the autogenerated null edit's summary
533 // and other texts chosen by dropdown menus on this page.
534 $maxlength = CommentStore::COMMENT_CHARACTER_LIMIT - 75;
535 $fields['wpProtectReasonSelection'] = [
536 'type' => 'select',
537 'cssclass' => 'mwProtect-reason',
538 'label' => $this->mContext->msg( 'protectcomment' )->text(),
539 'tabindex' => 4,
540 'id' => 'wpProtectReasonSelection',
541 'name' => 'wpProtectReasonSelection',
542 'flatlist' => true,
543 'options' => Xml::listDropDownOptions(
544 $this->mContext->msg( 'protect-dropdown' )->inContentLanguage()->text(),
545 [ 'other' => $this->mContext->msg( 'protect-otherreason-op' )->inContentLanguage()->text() ]
546 ),
547 'default' => $this->mReasonSelection,
548 ];
549 $fields['mwProtect-reason'] = [
550 'type' => 'text',
551 'id' => 'mwProtect-reason',
552 'label' => $this->mContext->msg( 'protect-otherreason' )->text(),
553 'name' => 'mwProtect-reason',
554 'size' => 60,
555 'maxlength' => $maxlength,
556 'default' => $this->mReason,
557 ];
558 # Disallow watching if user is not logged in
559 if ( $this->mPerformer->getUser()->isRegistered() ) {
560 $fields['mwProtectWatch'] = [
561 'type' => 'check',
562 'id' => 'mwProtectWatch',
563 'label' => $this->mContext->msg( 'watchthis' )->text(),
564 'name' => 'mwProtectWatch',
565 'default' => (
566 $this->watchlistManager->isWatched( $this->mPerformer, $this->mTitle )
567 || MediaWikiServices::getInstance()->getUserOptionsLookup()->getOption(
568 $this->mPerformer->getUser(),
569 'watchdefault'
570 )
571 ),
572 ];
573 }
574 }
575
576 if ( $this->mPerformer->isAllowed( 'editinterface' ) ) {
577 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
578 $link = $linkRenderer->makeKnownLink(
579 $this->mContext->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(),
580 $this->mContext->msg( 'protect-edit-reasonlist' )->text(),
581 [],
582 [ 'action' => 'edit' ]
583 );
584 $out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
585 }
586
587 if ( !$this->disabled ) {
588 $legacyUser = MediaWikiServices::getInstance()
589 ->getUserFactory()
590 ->newFromAuthority( $this->mPerformer );
591 $fields['wpEditToken'] = [
592 'name' => 'wpEditToken',
593 'type' => 'hidden',
594 'default' => $legacyUser->getEditToken( [ 'protect', $this->mTitle->getPrefixedDBkey() ] ),
595 ];
596 }
597
598 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->mContext );
599 $htmlForm
600 ->setMethod( 'post' )
601 ->setId( 'mw-Protect-Form' )
602 ->setTableId( 'mw-protect-table2' )
603 ->setAction( $this->mTitle->getLocalURL( 'action=protect' ) )
604 ->setSubmitID( 'mw-Protect-submit' )
605 ->setSubmitTextMsg( 'confirm' )
606 ->suppressDefaultSubmit( $this->disabled )
607 ->setWrapperLegendMsg( 'protect-legend' )
608 ->loadData();
609
610 return $htmlForm->getHTML( false ) . $out;
611 }
612
619 private function getOptionLabel( $permission ) {
620 if ( $permission == '' ) {
621 return $this->mContext->msg( 'protect-default' )->text();
622 } else {
623 // Messages: protect-level-autoconfirmed, protect-level-sysop
624 $msg = $this->mContext->msg( "protect-level-{$permission}" );
625 if ( $msg->exists() ) {
626 return $msg->text();
627 }
628 return $this->mContext->msg( 'protect-fallback', $permission )->text();
629 }
630 }
631
635 private function showLogExtract() {
636 # Show relevant lines from the protection log:
637 $protectLogPage = new LogPage( 'protect' );
638 $this->mOut->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
640 LogEventsList::showLogExtract( $this->mOut, 'protect', $this->mTitle );
641 # Let extensions add other relevant log extracts
642 $this->hookRunner->onProtectionForm__showLogExtract( $this->mArticle, $this->mOut );
643 }
644}
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.
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:49
getContext()
Gets the context this Article is executed in.
Definition Article.php:1954
getTitle()
Get the title object of the article.
Definition Article.php:224
An error page which can definitely be safely rendered using the OutputPage.
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:42
Class to simplify the use of log pages.
Definition LogPage.php:38
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()-...
A StatusValue for permission errors.
This is one of the Core classes and should be read at least once by any new developers.
setRobotPolicy( $policy)
Set the robot policy for the page: http://www.robotstxt.org/meta.html
Handles the page protection UI and backend.
__construct(Article $article)
PermissionStatus $mPermStatus
Permissions errors for the protect action.
execute()
Main entry point for action=protect and action=unprotect.
loadData()
Loads the current state of protection into the object.
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.
WebRequest $mRequest
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()
Show protection long extracts for this page.
Authority $mPerformer
PermissionManager $permManager
WatchlistManager $watchlistManager
getExpiry( $action)
Get the expiry time for a given action, by combining the relevant inputs.
Represents a title within MediaWiki.
Definition Title.php:48
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
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.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37