MediaWiki REL1_36
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 $mRequest;
81
83 private $mUser;
84
86 private $mLang;
87
89 private $mOut;
90
92 private $permManager;
93
95 private $hookRunner;
96
97 public function __construct( Article $article ) {
98 // Set instance variables.
99 $this->mArticle = $article;
100 $this->mTitle = $article->getTitle();
101 $this->mApplicableTypes = $this->mTitle->getRestrictionTypes();
102 $this->mContext = $article->getContext();
103 $this->mRequest = $this->mContext->getRequest();
104 $this->mUser = $this->mContext->getUser();
105 $this->mOut = $this->mContext->getOutput();
106 $this->mLang = $this->mContext->getLanguage();
107
108 $services = MediaWikiServices::getInstance();
109 $this->permManager = $services->getPermissionManager();
110 $this->hookRunner = new HookRunner( $services->getHookContainer() );
111
112 // Check if the form should be disabled.
113 // If it is, the form will be available in read-only to show levels.
114 $this->mPermErrors = $this->permManager->getPermissionErrors(
115 'protect',
116 $this->mUser,
117 $this->mTitle,
118 $this->mRequest->wasPosted()
119 ? PermissionManager::RIGOR_SECURE
120 : PermissionManager::RIGOR_FULL // T92357
121 );
122 if ( wfReadOnly() ) {
123 $this->mPermErrors[] = [ 'readonlytext', wfReadOnlyReason() ];
124 }
125 $this->disabled = $this->mPermErrors !== [];
126 $this->disabledAttrib = $this->disabled ? [ 'disabled' => 'disabled' ] : [];
127
128 $this->loadData();
129 }
130
134 private function loadData() {
135 $levels = $this->permManager->getNamespaceRestrictionLevels(
136 $this->mTitle->getNamespace(), $this->mUser
137 );
138
139 $this->mCascade = $this->mTitle->areRestrictionsCascading();
140 $this->mReason = $this->mRequest->getText( 'mwProtect-reason' );
141 $this->mReasonSelection = $this->mRequest->getText( 'wpProtectReasonSelection' );
142 $this->mCascade = $this->mRequest->getBool( 'mwProtect-cascade', $this->mCascade );
143
144 foreach ( $this->mApplicableTypes as $action ) {
145 // @todo FIXME: This form currently requires individual selections,
146 // but the db allows multiples separated by commas.
147
148 // Pull the actual restriction from the DB
149 $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
150
151 if ( !$this->mRestrictions[$action] ) {
152 // No existing expiry
153 $existingExpiry = '';
154 } else {
155 $existingExpiry = $this->mTitle->getRestrictionExpiry( $action );
156 }
157 $this->mExistingExpiry[$action] = $existingExpiry;
158
159 $requestExpiry = $this->mRequest->getText( "mwProtect-expiry-$action" );
160 $requestExpirySelection = $this->mRequest->getVal( "wpProtectExpirySelection-$action" );
161
162 if ( $requestExpiry ) {
163 // Custom expiry takes precedence
164 $this->mExpiry[$action] = $requestExpiry;
165 $this->mExpirySelection[$action] = 'othertime';
166 } elseif ( $requestExpirySelection ) {
167 // Expiry selected from list
168 $this->mExpiry[$action] = '';
169 $this->mExpirySelection[$action] = $requestExpirySelection;
170 } elseif ( $existingExpiry ) {
171 // Use existing expiry in its own list item
172 $this->mExpiry[$action] = '';
173 $this->mExpirySelection[$action] = $existingExpiry;
174 } else {
175 // Catches 'infinity' - Existing expiry is infinite, use "infinite" in drop-down
176 // Final default: infinite
177 $this->mExpiry[$action] = '';
178 $this->mExpirySelection[$action] = 'infinite';
179 }
180
181 $val = $this->mRequest->getVal( "mwProtect-level-$action" );
182 if ( isset( $val ) && in_array( $val, $levels ) ) {
183 $this->mRestrictions[$action] = $val;
184 }
185 }
186 }
187
195 private function getExpiry( $action ) {
196 if ( $this->mExpirySelection[$action] == 'existing' ) {
197 return $this->mExistingExpiry[$action];
198 } elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
199 $value = $this->mExpiry[$action];
200 } else {
201 $value = $this->mExpirySelection[$action];
202 }
203 if ( wfIsInfinity( $value ) ) {
204 $time = 'infinity';
205 } else {
206 $unix = strtotime( $value );
207
208 if ( !$unix || $unix === -1 ) {
209 return false;
210 }
211
212 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
213 // and there isn't notice about it in the ui
214 $time = wfTimestamp( TS_MW, $unix );
215 }
216 return $time;
217 }
218
222 public function execute() {
223 if (
224 $this->permManager->getNamespaceRestrictionLevels(
225 $this->mTitle->getNamespace()
226 ) === [ '' ]
227 ) {
228 throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
229 }
230
231 if ( $this->mRequest->wasPosted() ) {
232 if ( $this->save() ) {
233 $q = $this->mArticle->getPage()->isRedirect() ? 'redirect=no' : '';
234 $this->mOut->redirect( $this->mTitle->getFullURL( $q ) );
235 }
236 } else {
237 $this->show();
238 }
239 }
240
246 private function show( $err = null ) {
247 $out = $this->mOut;
248 $out->setRobotPolicy( 'noindex,nofollow' );
249 $out->addBacklinkSubtitle( $this->mTitle );
250
251 if ( is_array( $err ) ) {
252 $out->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", $err );
253 } elseif ( is_string( $err ) ) {
254 $out->addHTML( "<div class='error'>{$err}</div>\n" );
255 }
256
257 if ( $this->mApplicableTypes === [] ) {
258 // No restriction types available for the current title
259 // this might happen if an extension alters the available types
260 $out->setPageTitle( $this->mContext->msg(
261 'protect-norestrictiontypes-title',
262 $this->mTitle->getPrefixedText()
263 ) );
264 $out->addWikiTextAsInterface(
265 $this->mContext->msg( 'protect-norestrictiontypes-text' )->plain()
266 );
267
268 // Show the log in case protection was possible once
269 $this->showLogExtract();
270 // return as there isn't anything else we can do
271 return;
272 }
273
274 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
275 if ( $cascadeSources && count( $cascadeSources ) > 0 ) {
276 $titles = '';
277
278 foreach ( $cascadeSources as $title ) {
279 $titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
280 }
281
283 $out->wrapWikiMsg(
284 "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>",
285 [ 'protect-cascadeon', count( $cascadeSources ) ]
286 );
287 }
288
289 # Show an appropriate message if the user isn't allowed or able to change
290 # the protection settings at this time
291 if ( $this->disabled ) {
292 $out->setPageTitle(
293 $this->mContext->msg( 'protect-title-notallowed',
294 $this->mTitle->getPrefixedText() )
295 );
296 $out->addWikiTextAsInterface( $out->formatPermissionsErrorMessage(
297 $this->mPermErrors, 'protect'
298 ) );
299 } else {
300 $out->setPageTitle(
301 $this->mContext->msg( 'protect-title', $this->mTitle->getPrefixedText() )
302 );
303 $out->addWikiMsg( 'protect-text',
304 wfEscapeWikiText( $this->mTitle->getPrefixedText() ) );
305 }
306
307 $out->addHTML( $this->buildForm() );
308 $this->showLogExtract();
309 }
310
316 private function save() {
317 # Permission check!
318 if ( $this->disabled ) {
319 $this->show();
320 return false;
321 }
322
323 $token = $this->mRequest->getVal( 'wpEditToken' );
324 if ( !$this->mUser->matchEditToken( $token, [ 'protect', $this->mTitle->getPrefixedDBkey() ] ) ) {
325 $this->show( [ 'sessionfailure' ] );
326 return false;
327 }
328
329 # Create reason string. Use list and/or custom string.
330 $reasonstr = $this->mReasonSelection;
331 if ( $reasonstr != 'other' && $this->mReason != '' ) {
332 // Entry from drop down menu + additional comment
333 $reasonstr .= $this->mContext->msg( 'colon-separator' )->text() . $this->mReason;
334 } elseif ( $reasonstr == 'other' ) {
335 $reasonstr = $this->mReason;
336 }
337
338 $expiry = [];
339 foreach ( $this->mApplicableTypes as $action ) {
340 $expiry[$action] = $this->getExpiry( $action );
341 if ( empty( $this->mRestrictions[$action] ) ) {
342 // unprotected
343 continue;
344 }
345 if ( !$expiry[$action] ) {
346 $this->show( [ 'protect_expiry_invalid' ] );
347 return false;
348 }
349 if ( $expiry[$action] < wfTimestampNow() ) {
350 $this->show( [ 'protect_expiry_old' ] );
351 return false;
352 }
353 }
354
355 $this->mCascade = $this->mRequest->getBool( 'mwProtect-cascade' );
356
357 $status = $this->mArticle->getPage()->doUpdateRestrictions(
358 $this->mRestrictions,
359 $expiry,
360 $this->mCascade,
361 $reasonstr,
362 $this->mUser
363 );
364
365 if ( !$status->isOK() ) {
366 $this->show( $this->mOut->parseInlineAsInterface(
367 $status->getWikiText( false, false, $this->mLang )
368 ) );
369 return false;
370 }
371
378 $errorMsg = '';
379 if ( !$this->hookRunner->onProtectionForm__save( $this->mArticle, $errorMsg, $reasonstr ) ) {
380 if ( $errorMsg == '' ) {
381 $errorMsg = [ 'hookaborted' ];
382 }
383 }
384 if ( $errorMsg != '' ) {
385 $this->show( $errorMsg );
386 return false;
387 }
388
390 $this->mRequest->getCheck( 'mwProtectWatch' ),
391 $this->mTitle,
392 $this->mUser
393 );
394
395 return true;
396 }
397
403 private function buildForm() {
404 $this->mOut->enableOOUI();
405 $out = '';
406 $fields = [];
407 if ( !$this->disabled ) {
408 $this->mOut->addModules( 'mediawiki.action.protect' );
409 $this->mOut->addModuleStyles( 'mediawiki.action.styles' );
410 }
411 $scExpiryOptions = $this->mContext->msg( 'protect-expiry-options' )->inContentLanguage()->text();
412 $levels = $this->permManager->getNamespaceRestrictionLevels(
413 $this->mTitle->getNamespace(),
414 $this->disabled ? null : $this->mUser
415 );
416
417 // Not all languages have V_x <-> N_x relation
418 foreach ( $this->mRestrictions as $action => $selected ) {
419 // Messages:
420 // restriction-edit, restriction-move, restriction-create, restriction-upload
421 $section = 'restriction-' . $action;
422 $id = 'mwProtect-level-' . $action;
423 $options = [];
424 foreach ( $levels as $key ) {
425 $options[$this->getOptionLabel( $key )] = $key;
426 }
427
428 $fields[$id] = [
429 'type' => 'select',
430 'name' => $id,
431 'default' => $selected,
432 'id' => $id,
433 'size' => count( $levels ),
434 'options' => $options,
435 'disabled' => $this->disabled,
436 'section' => $section,
437 ];
438
439 $expiryOptions = [];
440
441 if ( $this->mExistingExpiry[$action] ) {
442 if ( $this->mExistingExpiry[$action] == 'infinity' ) {
443 $existingExpiryMessage = $this->mContext->msg( 'protect-existing-expiry-infinity' );
444 } else {
445 $timestamp = $this->mLang->userTimeAndDate( $this->mExistingExpiry[$action], $this->mUser );
446 $date = $this->mLang->userDate( $this->mExistingExpiry[$action], $this->mUser );
447 $time = $this->mLang->userTime( $this->mExistingExpiry[$action], $this->mUser );
448 $existingExpiryMessage = $this->mContext->msg(
449 'protect-existing-expiry',
450 $timestamp,
451 $date,
452 $time
453 );
454 }
455 $expiryOptions[$existingExpiryMessage->text()] = 'existing';
456 }
457
458 $expiryOptions[$this->mContext->msg( 'protect-othertime-op' )->text()] = 'othertime';
459 foreach ( explode( ',', $scExpiryOptions ) as $option ) {
460 if ( strpos( $option, ":" ) === false ) {
461 $show = $value = $option;
462 } else {
463 list( $show, $value ) = explode( ":", $option );
464 }
465 $expiryOptions[$show] = htmlspecialchars( $value );
466 }
467
468 # Add expiry dropdown
469 $fields["wpProtectExpirySelection-$action"] = [
470 'type' => 'select',
471 'name' => "wpProtectExpirySelection-$action",
472 'id' => "mwProtectExpirySelection-$action",
473 'tabindex' => '2',
474 'disabled' => $this->disabled,
475 'label' => $this->mContext->msg( 'protectexpiry' )->text(),
476 'options' => $expiryOptions,
477 'default' => $this->mExpirySelection[$action],
478 'section' => $section,
479 ];
480
481 # Add custom expiry field
482 if ( !$this->disabled ) {
483 $fields["mwProtect-expiry-$action"] = [
484 'type' => 'text',
485 'label' => $this->mContext->msg( 'protect-othertime' )->text(),
486 'name' => "mwProtect-expiry-$action",
487 'id' => "mwProtect-$action-expires",
488 'size' => 50,
489 'default' => $this->mExpiry[$action],
490 'disabled' => $this->disabled,
491 'section' => $section,
492 ];
493 }
494 }
495
496 # Give extensions a chance to add items to the form
497 $hookFormRaw = '';
498 $hookFormOptions = [];
499
500 $this->hookRunner->onProtectionForm__buildForm( $this->mArticle, $hookFormRaw );
501 $this->hookRunner->onProtectionFormAddFormFields( $this->mArticle, $hookFormOptions );
502
503 # Merge forms added from addFormFields
504 $fields = array_merge( $fields, $hookFormOptions );
505
506 # Add raw sections added in buildForm
507 if ( $hookFormRaw ) {
508 $fields['rawinfo'] = [
509 'type' => 'info',
510 'default' => $hookFormRaw,
511 'raw' => true,
512 'section' => 'restriction-blank'
513 ];
514 }
515
516 # JavaScript will add another row with a value-chaining checkbox
517 if ( $this->mTitle->exists() ) {
518 $fields['mwProtect-cascade'] = [
519 'type' => 'check',
520 'label' => $this->mContext->msg( 'protect-cascade' )->text(),
521 'id' => 'mwProtect-cascade',
522 'name' => 'mwProtect-cascade',
523 'default' => $this->mCascade,
524 'disabled' => $this->disabled,
525 ];
526 }
527
528 # Add manual and custom reason field/selects as well as submit
529 if ( !$this->disabled ) {
530 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
531 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
532 // Unicode codepoints.
533 // Subtract arbitrary 75 to leave some space for the autogenerated null edit's summary
534 // and other texts chosen by dropdown menus on this page.
535 $maxlength = CommentStore::COMMENT_CHARACTER_LIMIT - 75;
536 $fields['wpProtectReasonSelection'] = [
537 'type' => 'select',
538 'cssclass' => 'mwProtect-reason',
539 'label' => $this->mContext->msg( 'protectcomment' )->text(),
540 'tabindex' => 4,
541 'id' => 'wpProtectReasonSelection',
542 'name' => 'wpProtectReasonSelection',
543 'flatlist' => true,
544 'options' => Xml::listDropDownOptions(
545 $this->mContext->msg( 'protect-dropdown' )->inContentLanguage()->text(),
546 [ 'other' => $this->mContext->msg( 'protect-otherreason-op' )->inContentLanguage()->text() ]
547 ),
548 'default' => $this->mReasonSelection,
549 ];
550 $fields['mwProtect-reason'] = [
551 'type' => 'text',
552 'id' => 'mwProtect-reason',
553 'label' => $this->mContext->msg( 'protect-otherreason' )->text(),
554 'name' => 'mwProtect-reason',
555 'size' => 60,
556 'maxlength' => $maxlength,
557 'default' => $this->mReason,
558 ];
559 # Disallow watching if user is not logged in
560 if ( $this->mUser->isRegistered() ) {
561 $fields['mwProtectWatch'] = [
562 'type' => 'check',
563 'id' => 'mwProtectWatch',
564 'label' => $this->mContext->msg( 'watchthis' )->text(),
565 'name' => 'mwProtectWatch',
566 'default' => (
567 $this->mUser->isWatched( $this->mTitle )
568 || $this->mUser->getOption( 'watchdefault' )
569 ),
570 ];
571 }
572 }
573
574 if ( $this->permManager->userHasRight( $this->mUser, 'editinterface' ) ) {
575 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
576 $link = $linkRenderer->makeKnownLink(
577 $this->mContext->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(),
578 $this->mContext->msg( 'protect-edit-reasonlist' )->text(),
579 [],
580 [ 'action' => 'edit' ]
581 );
582 $out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
583 }
584
585 if ( !$this->disabled ) {
586 $fields['wpEditToken'] = [
587 'name' => 'wpEditToken',
588 'type' => 'hidden',
589 'default' => $this->mUser->getEditToken( [ 'protect', $this->mTitle->getPrefixedDBkey() ] ),
590 ];
591 }
592
593 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->mContext );
594 $htmlForm
595 ->setMethod( 'post' )
596 ->setId( 'mw-Protect-Form' )
597 ->setTableId( 'mw-protect-table2' )
598 ->setAction( $this->mTitle->getLocalURL( 'action=protect' ) )
599 ->setSubmitID( 'mw-Protect-submit' )
600 ->setSubmitText( $this->mContext->msg( 'confirm' )->text() )
601 ->suppressDefaultSubmit( $this->disabled )
602 ->setWrapperLegend( $this->mContext->msg( 'protect-legend' )->text() )
603 ->loadData();
604
605 return $htmlForm->getHTML( false ) . $out;
606 }
607
614 private function getOptionLabel( $permission ) {
615 if ( $permission == '' ) {
616 return $this->mContext->msg( 'protect-default' )->text();
617 } else {
618 // Messages: protect-level-autoconfirmed, protect-level-sysop
619 $msg = $this->mContext->msg( "protect-level-{$permission}" );
620 if ( $msg->exists() ) {
621 return $msg->text();
622 }
623 return $this->mContext->msg( 'protect-fallback', $permission )->text();
624 }
625 }
626
630 private function showLogExtract() {
631 # Show relevant lines from the protection log:
632 $protectLogPage = new LogPage( 'protect' );
633 $this->mOut->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
635 LogEventsList::showLogExtract( $this->mOut, 'protect', $this->mTitle );
636 # Let extensions add other relevant log extracts
637 $this->hookRunner->onProtectionForm__showLogExtract( $this->mArticle, $this->mOut );
638 }
639}
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:45
getContext()
Gets the context this Article is executed in.
Definition Article.php:2330
getTitle()
Get the title object of the article.
Definition Article.php:211
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:43
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()-...
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)
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.
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:48
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:67
static doWatchOrUnwatch( $watch, Title $title, Authority $performer, string $expiry=null)
Watch or unwatch a page.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Interface for objects which can provide a MediaWiki context on request.