Completed
Branch master (939199)
by
unknown
39:35
created

includes/ProtectionForm.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Page protection
4
 *
5
 * Copyright © 2005 Brion Vibber <[email protected]>
6
 * https://www.mediawiki.org/
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 */
25
26
/**
27
 * Handles the page protection UI and backend
28
 */
29
class ProtectionForm {
30
	/** @var array A map of action to restriction level, from request or default */
31
	protected $mRestrictions = [];
32
33
	/** @var string The custom/additional protection reason */
34
	protected $mReason = '';
35
36
	/** @var string The reason selected from the list, blank for other/additional */
37
	protected $mReasonSelection = '';
38
39
	/** @var bool True if the restrictions are cascading, from request or existing protection */
40
	protected $mCascade = false;
41
42
	/** @var array Map of action to "other" expiry time. Used in preference to mExpirySelection. */
43
	protected $mExpiry = [];
44
45
	/**
46
	 * @var array Map of action to value selected in expiry drop-down list.
47
	 * Will be set to 'othertime' whenever mExpiry is set.
48
	 */
49
	protected $mExpirySelection = [];
50
51
	/** @var array Permissions errors for the protect action */
52
	protected $mPermErrors = [];
53
54
	/** @var array Types (i.e. actions) for which levels can be selected */
55
	protected $mApplicableTypes = [];
56
57
	/** @var array Map of action to the expiry time of the existing protection */
58
	protected $mExistingExpiry = [];
59
60
	/** @var IContextSource */
61
	private $mContext;
62
63
	function __construct( Article $article ) {
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',
74
			$this->mContext->getUser(),
75
			$this->mContext->getRequest()->wasPosted() ? 'secure' : 'full' // T92357
76
		);
77
		if ( wfReadOnly() ) {
78
			$this->mPermErrors[] = [ 'readonlytext', wfReadOnlyReason() ];
79
		}
80
		$this->disabled = $this->mPermErrors != [];
81
		$this->disabledAttrib = $this->disabled
82
			? [ 'disabled' => 'disabled' ]
83
			: [];
84
85
		$this->loadData();
86
	}
87
88
	/**
89
	 * Loads the current state of protection into the object.
90
	 */
91
	function loadData() {
92
		$levels = MWNamespace::getRestrictionLevels(
93
			$this->mTitle->getNamespace(), $this->mContext->getUser()
94
		);
95
		$this->mCascade = $this->mTitle->areRestrictionsCascading();
96
97
		$request = $this->mContext->getRequest();
98
		$this->mReason = $request->getText( 'mwProtect-reason' );
99
		$this->mReasonSelection = $request->getText( 'wpProtectReasonSelection' );
100
		$this->mCascade = $request->getBool( 'mwProtect-cascade', $this->mCascade );
101
102
		foreach ( $this->mApplicableTypes as $action ) {
103
			// @todo FIXME: This form currently requires individual selections,
104
			// but the db allows multiples separated by commas.
105
106
			// Pull the actual restriction from the DB
107
			$this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
108
109
			if ( !$this->mRestrictions[$action] ) {
110
				// No existing expiry
111
				$existingExpiry = '';
112
			} else {
113
				$existingExpiry = $this->mTitle->getRestrictionExpiry( $action );
114
			}
115
			$this->mExistingExpiry[$action] = $existingExpiry;
116
117
			$requestExpiry = $request->getText( "mwProtect-expiry-$action" );
118
			$requestExpirySelection = $request->getVal( "wpProtectExpirySelection-$action" );
119
120
			if ( $requestExpiry ) {
121
				// Custom expiry takes precedence
122
				$this->mExpiry[$action] = $requestExpiry;
123
				$this->mExpirySelection[$action] = 'othertime';
124
			} elseif ( $requestExpirySelection ) {
125
				// Expiry selected from list
126
				$this->mExpiry[$action] = '';
127
				$this->mExpirySelection[$action] = $requestExpirySelection;
128
			} elseif ( $existingExpiry ) {
129
				// Use existing expiry in its own list item
130
				$this->mExpiry[$action] = '';
131
				$this->mExpirySelection[$action] = $existingExpiry;
132
			} else {
133
				// Catches 'infinity' - Existing expiry is infinite, use "infinite" in drop-down
134
				// Final default: infinite
135
				$this->mExpiry[$action] = '';
136
				$this->mExpirySelection[$action] = 'infinite';
137
			}
138
139
			$val = $request->getVal( "mwProtect-level-$action" );
140
			if ( isset( $val ) && in_array( $val, $levels ) ) {
141
				$this->mRestrictions[$action] = $val;
142
			}
143
		}
144
	}
145
146
	/**
147
	 * Get the expiry time for a given action, by combining the relevant inputs.
148
	 *
149
	 * @param string $action
150
	 *
151
	 * @return string 14-char timestamp or "infinity", or false if the input was invalid
152
	 */
153
	function getExpiry( $action ) {
154
		if ( $this->mExpirySelection[$action] == 'existing' ) {
155
			return $this->mExistingExpiry[$action];
156
		} elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
157
			$value = $this->mExpiry[$action];
158
		} else {
159
			$value = $this->mExpirySelection[$action];
160
		}
161 View Code Duplication
		if ( wfIsInfinity( $value ) ) {
162
			$time = 'infinity';
163
		} else {
164
			$unix = strtotime( $value );
165
166
			if ( !$unix || $unix === -1 ) {
167
				return false;
168
			}
169
170
			// @todo FIXME: Non-qualified absolute times are not in users specified timezone
171
			// and there isn't notice about it in the ui
172
			$time = wfTimestamp( TS_MW, $unix );
173
		}
174
		return $time;
175
	}
176
177
	/**
178
	 * Main entry point for action=protect and action=unprotect
179
	 */
180
	function execute() {
181
		if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === [ '' ] ) {
182
			throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
183
		}
184
185
		if ( $this->mContext->getRequest()->wasPosted() ) {
186
			if ( $this->save() ) {
187
				$q = $this->mArticle->isRedirect() ? 'redirect=no' : '';
188
				$this->mContext->getOutput()->redirect( $this->mTitle->getFullURL( $q ) );
189
			}
190
		} else {
191
			$this->show();
192
		}
193
	}
194
195
	/**
196
	 * Show the input form with optional error message
197
	 *
198
	 * @param string $err Error message or null if there's no error
199
	 */
200
	function show( $err = null ) {
201
		$out = $this->mContext->getOutput();
202
		$out->setRobotPolicy( 'noindex,nofollow' );
203
		$out->addBacklinkSubtitle( $this->mTitle );
204
205
		if ( is_array( $err ) ) {
206
			$out->wrapWikiMsg( "<p class='error'>\n$1\n</p>\n", $err );
207
		} elseif ( is_string( $err ) ) {
208
			$out->addHTML( "<p class='error'>{$err}</p>\n" );
209
		}
210
211
		if ( $this->mTitle->getRestrictionTypes() === [] ) {
212
			// No restriction types available for the current title
213
			// this might happen if an extension alters the available types
214
			$out->setPageTitle( $this->mContext->msg(
215
				'protect-norestrictiontypes-title',
216
				$this->mTitle->getPrefixedText()
217
			) );
218
			$out->addWikiText( $this->mContext->msg( 'protect-norestrictiontypes-text' )->plain() );
219
220
			// Show the log in case protection was possible once
221
			$this->showLogExtract( $out );
222
			// return as there isn't anything else we can do
223
			return;
224
		}
225
226
		list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
227
		if ( $cascadeSources && count( $cascadeSources ) > 0 ) {
228
			$titles = '';
229
230
			foreach ( $cascadeSources as $title ) {
231
				$titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
232
			}
233
234
			/** @todo FIXME: i18n issue, should use formatted number. */
235
			$out->wrapWikiMsg(
236
				"<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>",
237
				[ 'protect-cascadeon', count( $cascadeSources ) ]
238
			);
239
		}
240
241
		# Show an appropriate message if the user isn't allowed or able to change
242
		# the protection settings at this time
243
		if ( $this->disabled ) {
244
			$out->setPageTitle(
245
				$this->mContext->msg( 'protect-title-notallowed',
246
					$this->mTitle->getPrefixedText() )
247
			);
248
			$out->addWikiText( $out->formatPermissionsErrorMessage( $this->mPermErrors, 'protect' ) );
249
		} else {
250
			$out->setPageTitle( $this->mContext->msg( 'protect-title', $this->mTitle->getPrefixedText() ) );
251
			$out->addWikiMsg( 'protect-text',
252
				wfEscapeWikiText( $this->mTitle->getPrefixedText() ) );
253
		}
254
255
		$out->addHTML( $this->buildForm() );
256
		$this->showLogExtract( $out );
257
	}
258
259
	/**
260
	 * Save submitted protection form
261
	 *
262
	 * @return bool Success
263
	 */
264
	function save() {
265
		# Permission check!
266
		if ( $this->disabled ) {
267
			$this->show();
268
			return false;
269
		}
270
271
		$request = $this->mContext->getRequest();
272
		$user = $this->mContext->getUser();
273
		$out = $this->mContext->getOutput();
274
		$token = $request->getVal( 'wpEditToken' );
275
		if ( !$user->matchEditToken( $token, [ 'protect', $this->mTitle->getPrefixedDBkey() ] ) ) {
276
			$this->show( [ 'sessionfailure' ] );
277
			return false;
278
		}
279
280
		# Create reason string. Use list and/or custom string.
281
		$reasonstr = $this->mReasonSelection;
282
		if ( $reasonstr != 'other' && $this->mReason != '' ) {
283
			// Entry from drop down menu + additional comment
284
			$reasonstr .= $this->mContext->msg( 'colon-separator' )->text() . $this->mReason;
285
		} elseif ( $reasonstr == 'other' ) {
286
			$reasonstr = $this->mReason;
287
		}
288
		$expiry = [];
289
		foreach ( $this->mApplicableTypes as $action ) {
290
			$expiry[$action] = $this->getExpiry( $action );
291
			if ( empty( $this->mRestrictions[$action] ) ) {
292
				continue; // unprotected
293
			}
294
			if ( !$expiry[$action] ) {
295
				$this->show( [ 'protect_expiry_invalid' ] );
296
				return false;
297
			}
298
			if ( $expiry[$action] < wfTimestampNow() ) {
299
				$this->show( [ 'protect_expiry_old' ] );
300
				return false;
301
			}
302
		}
303
304
		$this->mCascade = $request->getBool( 'mwProtect-cascade' );
305
306
		$status = $this->mArticle->doUpdateRestrictions(
307
			$this->mRestrictions,
308
			$expiry,
309
			$this->mCascade,
310
			$reasonstr,
311
			$user
312
		);
313
314
		if ( !$status->isOK() ) {
315
			$this->show( $out->parseInline( $status->getWikiText() ) );
316
			return false;
317
		}
318
319
		/**
320
		 * Give extensions a change to handle added form items
321
		 *
322
		 * @since 1.19 you can (and you should) return false to abort saving;
323
		 *             you can also return an array of message name and its parameters
324
		 */
325
		$errorMsg = '';
326
		if ( !Hooks::run( 'ProtectionForm::save', [ $this->mArticle, &$errorMsg, $reasonstr ] ) ) {
327
			if ( $errorMsg == '' ) {
328
				$errorMsg = [ 'hookaborted' ];
329
			}
330
		}
331
		if ( $errorMsg != '' ) {
332
			$this->show( $errorMsg );
333
			return false;
334
		}
335
336
		WatchAction::doWatchOrUnwatch( $request->getCheck( 'mwProtectWatch' ), $this->mTitle, $user );
337
338
		return true;
339
	}
340
341
	/**
342
	 * Build the input form
343
	 *
344
	 * @return string HTML form
345
	 */
346
	function buildForm() {
347
		$context = $this->mContext;
348
		$user = $context->getUser();
349
		$output = $context->getOutput();
350
		$lang = $context->getLanguage();
351
		$cascadingRestrictionLevels = $context->getConfig()->get( 'CascadingRestrictionLevels' );
352
		$out = '';
353
		if ( !$this->disabled ) {
354
			$output->addModules( 'mediawiki.legacy.protect' );
355
			$output->addJsConfigVars( 'wgCascadeableLevels', $cascadingRestrictionLevels );
356
			$out .= Xml::openElement( 'form', [ 'method' => 'post',
357
				'action' => $this->mTitle->getLocalURL( 'action=protect' ),
358
				'id' => 'mw-Protect-Form' ] );
359
		}
360
361
		$out .= Xml::openElement( 'fieldset' ) .
362
			Xml::element( 'legend', null, $context->msg( 'protect-legend' )->text() ) .
363
			Xml::openElement( 'table', [ 'id' => 'mwProtectSet' ] ) .
364
			Xml::openElement( 'tbody' );
365
366
		$scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text();
367
		$showProtectOptions = $scExpiryOptions !== '-' && !$this->disabled;
368
369
		// Not all languages have V_x <-> N_x relation
370
		foreach ( $this->mRestrictions as $action => $selected ) {
371
			// Messages:
372
			// restriction-edit, restriction-move, restriction-create, restriction-upload
373
			$msg = $context->msg( 'restriction-' . $action );
374
			$out .= "<tr><td>" .
375
			Xml::openElement( 'fieldset' ) .
376
			Xml::element( 'legend', null, $msg->exists() ? $msg->text() : $action ) .
377
			Xml::openElement( 'table', [ 'id' => "mw-protect-table-$action" ] ) .
378
				"<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>";
379
380
			$mProtectexpiry = Xml::label(
381
				$context->msg( 'protectexpiry' )->text(),
382
				"mwProtectExpirySelection-$action"
383
			);
384
			$mProtectother = Xml::label(
385
				$context->msg( 'protect-othertime' )->text(),
386
				"mwProtect-$action-expires"
387
			);
388
389
			$expiryFormOptions = new XmlSelect(
390
				"wpProtectExpirySelection-$action",
391
				"mwProtectExpirySelection-$action",
392
				$this->mExpirySelection[$action]
393
			);
394
			$expiryFormOptions->setAttribute( 'tabindex', '2' );
395
			if ( $this->disabled ) {
396
				$expiryFormOptions->setAttribute( 'disabled', 'disabled' );
397
			}
398
399
			if ( $this->mExistingExpiry[$action] ) {
400
				if ( $this->mExistingExpiry[$action] == 'infinity' ) {
401
					$existingExpiryMessage = $context->msg( 'protect-existing-expiry-infinity' );
402
				} else {
403
					$timestamp = $lang->userTimeAndDate( $this->mExistingExpiry[$action], $user );
404
					$d = $lang->userDate( $this->mExistingExpiry[$action], $user );
405
					$t = $lang->userTime( $this->mExistingExpiry[$action], $user );
406
					$existingExpiryMessage = $context->msg(
407
						'protect-existing-expiry',
408
						$timestamp,
409
						$d,
410
						$t
411
					);
412
				}
413
				$expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
414
			}
415
416
			$expiryFormOptions->addOption(
417
				$context->msg( 'protect-othertime-op' )->text(),
418
				'othertime'
419
			);
420
			foreach ( explode( ',', $scExpiryOptions ) as $option ) {
421
				if ( strpos( $option, ":" ) === false ) {
422
					$show = $value = $option;
423
				} else {
424
					list( $show, $value ) = explode( ":", $option );
425
				}
426
				$expiryFormOptions->addOption( $show, htmlspecialchars( $value ) );
427
			}
428
			# Add expiry dropdown
429
			if ( $showProtectOptions && !$this->disabled ) {
430
				$out .= "
431
					<table><tr>
432
						<td class='mw-label'>
433
							{$mProtectexpiry}
434
						</td>
435
						<td class='mw-input'>" .
436
							$expiryFormOptions->getHTML() .
437
						"</td>
438
					</tr></table>";
439
			}
440
			# Add custom expiry field
441
			$attribs = [ 'id' => "mwProtect-$action-expires" ] + $this->disabledAttrib;
0 ignored issues
show
The property disabledAttrib does not seem to exist. Did you mean disabled?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
442
			$out .= "<table><tr>
443
					<td class='mw-label'>" .
444
						$mProtectother .
445
					'</td>
446
					<td class="mw-input">' .
447
						Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) .
448
					'</td>
449
				</tr></table>';
450
			$out .= "</td></tr>" .
451
			Xml::closeElement( 'table' ) .
452
			Xml::closeElement( 'fieldset' ) .
453
			"</td></tr>";
454
		}
455
		# Give extensions a chance to add items to the form
456
		Hooks::run( 'ProtectionForm::buildForm', [ $this->mArticle, &$out ] );
457
458
		$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
459
460
		// JavaScript will add another row with a value-chaining checkbox
461
		if ( $this->mTitle->exists() ) {
462
			$out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table2' ] ) .
463
				Xml::openElement( 'tbody' );
464
			$out .= '<tr>
465
					<td></td>
466
					<td class="mw-input">' .
467
						Xml::checkLabel(
468
							$context->msg( 'protect-cascade' )->text(),
469
							'mwProtect-cascade',
470
							'mwProtect-cascade',
471
							$this->mCascade, $this->disabledAttrib
472
						) .
473
					"</td>
474
				</tr>\n";
475
			$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
476
		}
477
478
		# Add manual and custom reason field/selects as well as submit
479
		if ( !$this->disabled ) {
480
			$mProtectreasonother = Xml::label(
481
				$context->msg( 'protectcomment' )->text(),
482
				'wpProtectReasonSelection'
483
			);
484
485
			$mProtectreason = Xml::label(
486
				$context->msg( 'protect-otherreason' )->text(),
487
				'mwProtect-reason'
488
			);
489
490
			$reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
491
				wfMessage( 'protect-dropdown' )->inContentLanguage()->text(),
492
				wfMessage( 'protect-otherreason-op' )->inContentLanguage()->text(),
493
				$this->mReasonSelection,
494
				'mwProtect-reason', 4 );
495
496
			$out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table3' ] ) .
497
				Xml::openElement( 'tbody' );
498
			$out .= "
499
				<tr>
500
					<td class='mw-label'>
501
						{$mProtectreasonother}
502
					</td>
503
					<td class='mw-input'>
504
						{$reasonDropDown}
505
					</td>
506
				</tr>
507
				<tr>
508
					<td class='mw-label'>
509
						{$mProtectreason}
510
					</td>
511
					<td class='mw-input'>" .
512
						Xml::input( 'mwProtect-reason', 60, $this->mReason, [ 'type' => 'text',
513
							'id' => 'mwProtect-reason', 'maxlength' => 180 ] ) .
514
							// Limited maxlength as the database trims at 255 bytes and other texts
515
							// chosen by dropdown menus on this page are also included in this database field.
516
							// The byte limit of 180 bytes is enforced in javascript
517
					"</td>
518
				</tr>";
519
			# Disallow watching is user is not logged in
520
			if ( $user->isLoggedIn() ) {
521
				$out .= "
522
				<tr>
523
					<td></td>
524
					<td class='mw-input'>" .
525
						Xml::checkLabel( $context->msg( 'watchthis' )->text(),
526
							'mwProtectWatch', 'mwProtectWatch',
527
							$user->isWatched( $this->mTitle ) || $user->getOption( 'watchdefault' ) ) .
528
					"</td>
529
				</tr>";
530
			}
531
			$out .= "
532
				<tr>
533
					<td></td>
534
					<td class='mw-submit'>" .
535
						Xml::submitButton(
536
							$context->msg( 'confirm' )->text(),
537
							[ 'id' => 'mw-Protect-submit' ]
538
						) .
539
					"</td>
540
				</tr>\n";
541
			$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
542
		}
543
		$out .= Xml::closeElement( 'fieldset' );
544
545 View Code Duplication
		if ( $user->isAllowed( 'editinterface' ) ) {
546
			$link = Linker::linkKnown(
547
				$context->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(),
548
				$context->msg( 'protect-edit-reasonlist' )->escaped(),
549
				[],
550
				[ 'action' => 'edit' ]
551
			);
552
			$out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
553
		}
554
555
		if ( !$this->disabled ) {
556
			$out .= Html::hidden(
557
				'wpEditToken',
558
				$user->getEditToken( [ 'protect', $this->mTitle->getPrefixedDBkey() ] )
559
			);
560
			$out .= Xml::closeElement( 'form' );
561
		}
562
563
		return $out;
564
	}
565
566
	/**
567
	 * Build protection level selector
568
	 *
569
	 * @param string $action Action to protect
570
	 * @param string $selected Current protection level
571
	 * @return string HTML fragment
572
	 */
573
	function buildSelector( $action, $selected ) {
574
		// If the form is disabled, display all relevant levels. Otherwise,
575
		// just show the ones this user can use.
576
		$levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(),
577
			$this->disabled ? null : $this->mContext->getUser()
578
		);
579
580
		$id = 'mwProtect-level-' . $action;
581
582
		$select = new XmlSelect( $id, $id, $selected );
583
		$select->setAttribute( 'size', count( $levels ) );
584
		if ( $this->disabled ) {
585
			$select->setAttribute( 'disabled', 'disabled' );
586
		}
587
588
		foreach ( $levels as $key ) {
589
			$select->addOption( $this->getOptionLabel( $key ), $key );
590
		}
591
592
		return $select->getHTML();
593
	}
594
595
	/**
596
	 * Prepare the label for a protection selector option
597
	 *
598
	 * @param string $permission Permission required
599
	 * @return string
600
	 */
601
	private function getOptionLabel( $permission ) {
602
		if ( $permission == '' ) {
603
			return $this->mContext->msg( 'protect-default' )->text();
604
		} else {
605
			// Messages: protect-level-autoconfirmed, protect-level-sysop
606
			$msg = $this->mContext->msg( "protect-level-{$permission}" );
607
			if ( $msg->exists() ) {
608
				return $msg->text();
609
			}
610
			return $this->mContext->msg( 'protect-fallback', $permission )->text();
611
		}
612
	}
613
614
	/**
615
	 * Show protection long extracts for this page
616
	 *
617
	 * @param OutputPage $out
618
	 * @access private
619
	 */
620
	function showLogExtract( &$out ) {
621
		# Show relevant lines from the protection log:
622
		$protectLogPage = new LogPage( 'protect' );
623
		$out->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) );
624
		LogEventsList::showLogExtract( $out, 'protect', $this->mTitle );
625
		# Let extensions add other relevant log extracts
626
		Hooks::run( 'ProtectionForm::showLogExtract', [ $this->mArticle, $out ] );
627
	}
628
}
629