MediaWiki REL1_39
ProtectionForm.php
Go to the documentation of this file.
1<?php
33
39 protected $mRestrictions = [];
40
42 protected $mReason = '';
43
45 protected $mReasonSelection = '';
46
48 protected $mCascade = false;
49
51 protected $mExpiry = [];
52
57 protected $mExpirySelection = [];
58
60 protected $mPermStatus;
61
63 protected $mApplicableTypes = [];
64
66 protected $mExistingExpiry = [];
67
69 protected $mArticle;
70
72 protected $mTitle;
73
75 protected $disabled;
76
78 protected $disabledAttrib;
79
81 private $mContext;
82
84 private $mRequest;
85
87 private $mPerformer;
88
90 private $mLang;
91
93 private $mOut;
94
96 private $permManager;
97
101 private $watchlistManager;
102
104 private $hookRunner;
105
107 private $restrictionStore;
108
110 private $titleFormatter;
111
112 public function __construct( Article $article ) {
113 // Set instance variables.
114 $this->mArticle = $article;
115 $this->mTitle = $article->getTitle();
116 $this->mContext = $article->getContext();
117 $this->mRequest = $this->mContext->getRequest();
118 $this->mPerformer = $this->mContext->getAuthority();
119 $this->mOut = $this->mContext->getOutput();
120 $this->mLang = $this->mContext->getLanguage();
121
122 $services = MediaWikiServices::getInstance();
123 $this->permManager = $services->getPermissionManager();
124 $this->hookRunner = new HookRunner( $services->getHookContainer() );
125 $this->watchlistManager = $services->getWatchlistManager();
126 $this->titleFormatter = $services->getTitleFormatter();
127 $this->restrictionStore = $services->getRestrictionStore();
128 $this->mApplicableTypes = $this->restrictionStore->listApplicableRestrictionTypes( $this->mTitle );
129
130 // Check if the form should be disabled.
131 // If it is, the form will be available in read-only to show levels.
132 $this->mPermStatus = PermissionStatus::newEmpty();
133 if ( $this->mRequest->wasPosted() ) {
134 $this->mPerformer->authorizeWrite( 'protect', $this->mTitle, $this->mPermStatus );
135 } else {
136 $this->mPerformer->authorizeRead( 'protect', $this->mTitle, $this->mPermStatus );
137 }
138 $readOnlyMode = $services->getReadOnlyMode();
139 if ( $readOnlyMode->isReadOnly() ) {
140 $this->mPermStatus->fatal( 'readonlytext', $readOnlyMode->getReason() );
141 }
142 $this->disabled = !$this->mPermStatus->isGood();
143 $this->disabledAttrib = $this->disabled ? [ 'disabled' => 'disabled' ] : [];
144
145 $this->loadData();
146 }
147
151 private function loadData() {
152 $levels = $this->permManager->getNamespaceRestrictionLevels(
153 $this->mTitle->getNamespace(), $this->mPerformer->getUser()
154 );
155
156 $this->mCascade = $this->restrictionStore->areRestrictionsCascading( $this->mTitle );
157 $this->mReason = $this->mRequest->getText( 'mwProtect-reason' );
158 $this->mReasonSelection = $this->mRequest->getText( 'wpProtectReasonSelection' );
159 $this->mCascade = $this->mRequest->getBool( 'mwProtect-cascade', $this->mCascade );
160
161 foreach ( $this->mApplicableTypes as $action ) {
162 // @todo FIXME: This form currently requires individual selections,
163 // but the db allows multiples separated by commas.
164
165 // Pull the actual restriction from the DB
166 $this->mRestrictions[$action] = implode( '',
167 $this->restrictionStore->getRestrictions( $this->mTitle, $action ) );
168
169 if ( !$this->mRestrictions[$action] ) {
170 // No existing expiry
171 $existingExpiry = '';
172 } else {
173 $existingExpiry = $this->restrictionStore->getRestrictionExpiry( $this->mTitle, $action );
174 }
175 $this->mExistingExpiry[$action] = $existingExpiry;
176
177 $requestExpiry = $this->mRequest->getText( "mwProtect-expiry-$action" );
178 $requestExpirySelection = $this->mRequest->getVal( "wpProtectExpirySelection-$action" );
179
180 if ( $requestExpiry ) {
181 // Custom expiry takes precedence
182 $this->mExpiry[$action] = $requestExpiry;
183 $this->mExpirySelection[$action] = 'othertime';
184 } elseif ( $requestExpirySelection ) {
185 // Expiry selected from list
186 $this->mExpiry[$action] = '';
187 $this->mExpirySelection[$action] = $requestExpirySelection;
188 } elseif ( $existingExpiry ) {
189 // Use existing expiry in its own list item
190 $this->mExpiry[$action] = '';
191 $this->mExpirySelection[$action] = $existingExpiry;
192 } else {
193 // Catches 'infinity' - Existing expiry is infinite, use "infinite" in drop-down
194 // Final default: infinite
195 $this->mExpiry[$action] = '';
196 $this->mExpirySelection[$action] = 'infinite';
197 }
198
199 $val = $this->mRequest->getVal( "mwProtect-level-$action" );
200 if ( isset( $val ) && in_array( $val, $levels ) ) {
201 $this->mRestrictions[$action] = $val;
202 }
203 }
204 }
205
213 private function getExpiry( $action ) {
214 if ( $this->mExpirySelection[$action] == 'existing' ) {
215 return $this->mExistingExpiry[$action];
216 } elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
217 $value = $this->mExpiry[$action];
218 } else {
219 $value = $this->mExpirySelection[$action];
220 }
221 if ( wfIsInfinity( $value ) ) {
222 $time = 'infinity';
223 } else {
224 $unix = strtotime( $value );
225
226 if ( !$unix || $unix === -1 ) {
227 return false;
228 }
229
230 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
231 // and there isn't notice about it in the ui
232 $time = wfTimestamp( TS_MW, $unix );
233 }
234 return $time;
235 }
236
240 public function execute() {
241 if (
242 $this->permManager->getNamespaceRestrictionLevels(
243 $this->mTitle->getNamespace()
244 ) === [ '' ]
245 ) {
246 throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
247 }
248
249 if ( $this->mRequest->wasPosted() ) {
250 if ( $this->save() ) {
251 $q = $this->mArticle->getPage()->isRedirect() ? 'redirect=no' : '';
252 $this->mOut->redirect( $this->mTitle->getFullURL( $q ) );
253 }
254 } else {
255 $this->show();
256 }
257 }
258
265 private function show( $err = null ) {
266 $out = $this->mOut;
267 $out->setRobotPolicy( 'noindex,nofollow' );
268 $out->addBacklinkSubtitle( $this->mTitle );
269
270 if ( is_array( $err ) ) {
271 $out->addHTML( Html::errorBox( $out->msg( ...$err )->parse() ) );
272 } elseif ( is_string( $err ) ) {
273 $out->addHTML( Html::errorBox( $err ) );
274 }
275
276 if ( $this->mApplicableTypes === [] ) {
277 // No restriction types available for the current title
278 // this might happen if an extension alters the available types
279 $out->setPageTitle( $this->mContext->msg(
280 'protect-norestrictiontypes-title',
281 $this->mTitle->getPrefixedText()
282 ) );
283 $out->addWikiTextAsInterface(
284 $this->mContext->msg( 'protect-norestrictiontypes-text' )->plain()
285 );
286
287 // Show the log in case protection was possible once
288 $this->showLogExtract();
289 // return as there isn't anything else we can do
290 return;
291 }
292
293 list( $cascadeSources, /* $restrictions */ ) =
294 $this->restrictionStore->getCascadeProtectionSources( $this->mTitle );
295 if ( count( $cascadeSources ) > 0 ) {
296 $titles = '';
297
298 foreach ( $cascadeSources as $pageIdentity ) {
299 $titles .= '* [[:' . $this->titleFormatter->getPrefixedText( $pageIdentity ) . "]]\n";
300 }
301
303 $out->wrapWikiMsg(
304 "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>",
305 [ 'protect-cascadeon', count( $cascadeSources ) ]
306 );
307 }
308
309 # Show an appropriate message if the user isn't allowed or able to change
310 # the protection settings at this time
311 if ( $this->disabled ) {
312 $out->setPageTitle(
313 $this->mContext->msg( 'protect-title-notallowed',
314 $this->mTitle->getPrefixedText() )
315 );
316 $out->addWikiTextAsInterface(
317 $out->formatPermissionStatus( $this->mPermStatus, 'protect' )
318 );
319 } else {
320 $out->setPageTitle(
321 $this->mContext->msg( 'protect-title', $this->mTitle->getPrefixedText() )
322 );
323 $out->addWikiMsg( 'protect-text',
324 wfEscapeWikiText( $this->mTitle->getPrefixedText() ) );
325 }
326
327 $out->addHTML( $this->buildForm() );
328 $this->showLogExtract();
329 }
330
336 private function save() {
337 # Permission check!
338 if ( $this->disabled ) {
339 $this->show();
340 return false;
341 }
342
343 $token = $this->mRequest->getVal( 'wpEditToken' );
344 $legacyUser = MediaWikiServices::getInstance()
345 ->getUserFactory()
346 ->newFromAuthority( $this->mPerformer );
347 if ( !$legacyUser->matchEditToken( $token, [ 'protect', $this->mTitle->getPrefixedDBkey() ] ) ) {
348 $this->show( [ 'sessionfailure' ] );
349 return false;
350 }
351
352 # Create reason string. Use list and/or custom string.
353 $reasonstr = $this->mReasonSelection;
354 if ( $reasonstr != 'other' && $this->mReason != '' ) {
355 // Entry from drop down menu + additional comment
356 $reasonstr .= $this->mContext->msg( 'colon-separator' )->text() . $this->mReason;
357 } elseif ( $reasonstr == 'other' ) {
358 $reasonstr = $this->mReason;
359 }
360
361 $expiry = [];
362 foreach ( $this->mApplicableTypes as $action ) {
363 $expiry[$action] = $this->getExpiry( $action );
364 if ( empty( $this->mRestrictions[$action] ) ) {
365 // unprotected
366 continue;
367 }
368 if ( !$expiry[$action] ) {
369 $this->show( [ 'protect_expiry_invalid' ] );
370 return false;
371 }
372 if ( $expiry[$action] < wfTimestampNow() ) {
373 $this->show( [ 'protect_expiry_old' ] );
374 return false;
375 }
376 }
377
378 $this->mCascade = $this->mRequest->getBool( 'mwProtect-cascade' );
379
380 $status = $this->mArticle->getPage()->doUpdateRestrictions(
381 $this->mRestrictions,
382 $expiry,
383 $this->mCascade,
384 $reasonstr,
385 $this->mPerformer->getUser()
386 );
387
388 if ( !$status->isOK() ) {
389 $this->show( $this->mOut->parseInlineAsInterface(
390 $status->getWikiText( false, false, $this->mLang )
391 ) );
392 return false;
393 }
394
401 $errorMsg = '';
402 if ( !$this->hookRunner->onProtectionForm__save( $this->mArticle, $errorMsg, $reasonstr ) ) {
403 if ( $errorMsg == '' ) {
404 $errorMsg = [ 'hookaborted' ];
405 }
406 }
407 if ( $errorMsg != '' ) {
408 $this->show( $errorMsg );
409 return false;
410 }
411
412 $this->watchlistManager->setWatch(
413 $this->mRequest->getCheck( 'mwProtectWatch' ),
414 $this->mPerformer,
415 $this->mTitle
416 );
417
418 return true;
419 }
420
426 private function buildForm() {
427 $this->mOut->enableOOUI();
428 $out = '';
429 $fields = [];
430 if ( !$this->disabled ) {
431 $this->mOut->addModules( 'mediawiki.action.protect' );
432 $this->mOut->addModuleStyles( 'mediawiki.action.styles' );
433 }
434 $scExpiryOptions = $this->mContext->msg( 'protect-expiry-options' )->inContentLanguage()->text();
435 $levels = $this->permManager->getNamespaceRestrictionLevels(
436 $this->mTitle->getNamespace(),
437 $this->disabled ? null : $this->mPerformer->getUser()
438 );
439
440 // Not all languages have V_x <-> N_x relation
441 foreach ( $this->mRestrictions as $action => $selected ) {
442 // Messages:
443 // restriction-edit, restriction-move, restriction-create, restriction-upload
444 $section = 'restriction-' . $action;
445 $id = 'mwProtect-level-' . $action;
446 $options = [];
447 foreach ( $levels as $key ) {
448 $options[$this->getOptionLabel( $key )] = $key;
449 }
450
451 $fields[$id] = [
452 'type' => 'select',
453 'name' => $id,
454 'default' => $selected,
455 'id' => $id,
456 'size' => count( $levels ),
457 'options' => $options,
458 'disabled' => $this->disabled,
459 'section' => $section,
460 ];
461
462 $expiryOptions = [];
463
464 if ( $this->mExistingExpiry[$action] ) {
465 if ( $this->mExistingExpiry[$action] == 'infinity' ) {
466 $existingExpiryMessage = $this->mContext->msg( 'protect-existing-expiry-infinity' );
467 } else {
468 $existingExpiryMessage = $this->mContext->msg( 'protect-existing-expiry' )
469 ->dateTimeParams( $this->mExistingExpiry[$action] )
470 ->dateParams( $this->mExistingExpiry[$action] )
471 ->timeParams( $this->mExistingExpiry[$action] );
472 }
473 $expiryOptions[$existingExpiryMessage->text()] = 'existing';
474 }
475
476 $expiryOptions[$this->mContext->msg( 'protect-othertime-op' )->text()] = 'othertime';
477
478 $expiryOptions = array_merge( $expiryOptions, XmlSelect::parseOptionsMessage( $scExpiryOptions ) );
479
480 # Add expiry dropdown
481 $fields["wpProtectExpirySelection-$action"] = [
482 'type' => 'select',
483 'name' => "wpProtectExpirySelection-$action",
484 'id' => "mwProtectExpirySelection-$action",
485 'tabindex' => '2',
486 'disabled' => $this->disabled,
487 'label' => $this->mContext->msg( 'protectexpiry' )->text(),
488 'options' => $expiryOptions,
489 'default' => $this->mExpirySelection[$action],
490 'section' => $section,
491 ];
492
493 # Add custom expiry field
494 if ( !$this->disabled ) {
495 $fields["mwProtect-expiry-$action"] = [
496 'type' => 'text',
497 'label' => $this->mContext->msg( 'protect-othertime' )->text(),
498 'name' => "mwProtect-expiry-$action",
499 'id' => "mwProtect-$action-expires",
500 'size' => 50,
501 'default' => $this->mExpiry[$action],
502 'disabled' => $this->disabled,
503 'section' => $section,
504 ];
505 }
506 }
507
508 # Give extensions a chance to add items to the form
509 $hookFormRaw = '';
510 $hookFormOptions = [];
511
512 $this->hookRunner->onProtectionForm__buildForm( $this->mArticle, $hookFormRaw );
513 $this->hookRunner->onProtectionFormAddFormFields( $this->mArticle, $hookFormOptions );
514
515 # Merge forms added from addFormFields
516 $fields = array_merge( $fields, $hookFormOptions );
517
518 # Add raw sections added in buildForm
519 if ( $hookFormRaw ) {
520 $fields['rawinfo'] = [
521 'type' => 'info',
522 'default' => $hookFormRaw,
523 'raw' => true,
524 'section' => 'restriction-blank'
525 ];
526 }
527
528 # JavaScript will add another row with a value-chaining checkbox
529 if ( $this->mTitle->exists() ) {
530 $fields['mwProtect-cascade'] = [
531 'type' => 'check',
532 'label' => $this->mContext->msg( 'protect-cascade' )->text(),
533 'id' => 'mwProtect-cascade',
534 'name' => 'mwProtect-cascade',
535 'default' => $this->mCascade,
536 'disabled' => $this->disabled,
537 ];
538 }
539
540 # Add manual and custom reason field/selects as well as submit
541 if ( !$this->disabled ) {
542 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
543 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
544 // Unicode codepoints.
545 // Subtract arbitrary 75 to leave some space for the autogenerated null edit's summary
546 // and other texts chosen by dropdown menus on this page.
548 $fields['wpProtectReasonSelection'] = [
549 'type' => 'select',
550 'cssclass' => 'mwProtect-reason',
551 'label' => $this->mContext->msg( 'protectcomment' )->text(),
552 'tabindex' => 4,
553 'id' => 'wpProtectReasonSelection',
554 'name' => 'wpProtectReasonSelection',
555 'flatlist' => true,
556 'options' => Xml::listDropDownOptions(
557 $this->mContext->msg( 'protect-dropdown' )->inContentLanguage()->text(),
558 [ 'other' => $this->mContext->msg( 'protect-otherreason-op' )->inContentLanguage()->text() ]
559 ),
560 'default' => $this->mReasonSelection,
561 ];
562 $fields['mwProtect-reason'] = [
563 'type' => 'text',
564 'id' => 'mwProtect-reason',
565 'label' => $this->mContext->msg( 'protect-otherreason' )->text(),
566 'name' => 'mwProtect-reason',
567 'size' => 60,
568 'maxlength' => $maxlength,
569 'default' => $this->mReason,
570 ];
571 # Disallow watching if user is not logged in
572 if ( $this->mPerformer->getUser()->isRegistered() ) {
573 $fields['mwProtectWatch'] = [
574 'type' => 'check',
575 'id' => 'mwProtectWatch',
576 'label' => $this->mContext->msg( 'watchthis' )->text(),
577 'name' => 'mwProtectWatch',
578 'default' => (
579 $this->watchlistManager->isWatched( $this->mPerformer, $this->mTitle )
580 || MediaWikiServices::getInstance()->getUserOptionsLookup()->getOption(
581 $this->mPerformer->getUser(),
582 'watchdefault'
583 )
584 ),
585 ];
586 }
587 }
588
589 if ( $this->mPerformer->isAllowed( 'editinterface' ) ) {
590 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
591 $link = $linkRenderer->makeKnownLink(
592 $this->mContext->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(),
593 $this->mContext->msg( 'protect-edit-reasonlist' )->text(),
594 [],
595 [ 'action' => 'edit' ]
596 );
597 $out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
598 }
599
600 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->mContext );
601 $htmlForm
602 ->setMethod( 'post' )
603 ->setId( 'mw-Protect-Form' )
604 ->setTableId( 'mw-protect-table2' )
605 ->setAction( $this->mTitle->getLocalURL( 'action=protect' ) )
606 ->setSubmitID( 'mw-Protect-submit' )
607 ->setSubmitTextMsg( 'confirm' )
608 ->setTokenSalt( [ 'protect', $this->mTitle->getPrefixedDBkey() ] )
609 ->suppressDefaultSubmit( $this->disabled )
610 ->setWrapperLegendMsg( 'protect-legend' )
611 ->prepareForm();
612
613 return $htmlForm->getHTML( false ) . $out;
614 }
615
622 private function getOptionLabel( $permission ) {
623 if ( $permission == '' ) {
624 return $this->mContext->msg( 'protect-default' )->text();
625 } else {
626 // Messages: protect-level-autoconfirmed, protect-level-sysop
627 $msg = $this->mContext->msg( "protect-level-{$permission}" );
628 if ( $msg->exists() ) {
629 return $msg->text();
630 }
631 return $this->mContext->msg( 'protect-fallback', $permission )->text();
632 }
633 }
634
638 private function showLogExtract() {
639 # Show relevant lines from the protection log:
640 $protectLogPage = new LogPage( 'protect' );
641 $this->mOut->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
643 LogEventsList::showLogExtract( $this->mOut, 'protect', $this->mTitle );
644 # Let extensions add other relevant log extracts
645 $this->hookRunner->onProtectionForm__showLogExtract( $this->mArticle, $this->mOut );
646 }
647}
getUser()
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
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,...
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:48
getContext()
Gets the context this Article is executed in.
Definition Article.php:2013
getTitle()
Get the title object of the article.
Definition Article.php:230
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
An error page which can definitely be safely rendered using the OutputPage.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition HTMLForm.php:348
Base class for language-specific code.
Definition Language.php:53
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:39
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service locator for MediaWiki core services.
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.
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.
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.
string $mReason
The custom/additional protection reason.
array $mExpiry
Map of action to "other" expiry time.
Represents a title within MediaWiki.
Definition Title.php:49
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...
static listDropDownOptions( $list, $params=[])
Build options for a drop-down box from a textual list.
Definition Xml.php:550
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
A title formatter service for MediaWiki.