MediaWiki master
IntroMessageBuilder.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\EditPage;
4
5use LogicException;
35
44
45 use ParametersHelper;
46
47 // Parameters for getIntroMessages()
48 public const MORE_FRAMES = 1;
49 public const LESS_FRAMES = 2;
50
51 public function __construct(
52 private readonly Config $config,
53 private readonly LinkRenderer $linkRenderer,
54 private readonly PermissionManager $permManager,
55 private readonly UserNameUtils $userNameUtils,
56 private readonly TempUserCreator $tempUserCreator,
57 private readonly UserFactory $userFactory,
58 private readonly RestrictionStore $restrictionStore,
59 private readonly DatabaseBlockStore $blockStore,
60 private readonly ReadOnlyMode $readOnlyMode,
61 private readonly SpecialPageFactory $specialPageFactory,
62 private readonly RepoGroup $repoGroup,
63 private readonly NamespaceInfo $namespaceInfo,
64 private readonly SkinFactory $skinFactory,
65 private readonly IConnectionProvider $dbProvider,
66 private readonly UrlUtils $urlUtils,
67 private readonly ShadowPageLoader $shadowPageLoader,
68 ) {
69 }
70
82 private function getLogExtract( $types = [], $page = '', $user = '', $param = [] ): string {
83 $outString = '';
84 LogEventsList::showLogExtract( $outString, $types, $page, $user, $param );
85 return $outString;
86 }
87
112 public function getIntroMessages(
113 int $frames,
114 array $skip,
115 MessageLocalizer $localizer,
116 ProperPageIdentity $page,
117 ?RevisionRecord $revRecord,
118 Authority $performer,
119 ?string $editIntro,
120 ?string $returnToQuery,
121 bool $preview,
122 ?string $section = null
123 ): array {
124 $title = Title::newFromPageIdentity( $page );
125 $messages = new IntroMessageList( $frames, $skip );
126
127 $this->addOldRevisionWarning( $messages, $localizer, $revRecord );
128
129 if ( !$preview ) {
130 $this->addCodeEditingIntro( $messages, $localizer, $title, $performer );
131 $this->addSharedRepoHint( $messages, $localizer, $page );
132 $this->addUserWarnings( $messages, $localizer, $title, $performer );
133 $this->addEditIntro( $messages, $localizer, $page, $performer, $editIntro, $section );
134 $this->addRecreateWarning( $messages, $page );
135 }
136
137 $this->addTalkPageText( $messages, $localizer, $title );
138 $this->addEditNotices( $messages, $localizer, $title, $revRecord );
139
140 $this->addReadOnlyWarning( $messages, $localizer );
141 $this->addAnonEditWarning( $messages, $localizer, $title, $performer, $returnToQuery, $preview );
142 $this->addUserConfigPageInfo( $messages, $localizer, $title, $performer, $preview );
143 $this->addPageProtectionWarningHeaders( $messages, $localizer, $page );
144 $this->addHeaderCopyrightWarning( $messages, $localizer );
145
146 return $messages->getList();
147 }
148
152 private function addCodeEditingIntro(
153 IntroMessageList $messages,
154 MessageLocalizer $localizer,
155 Title $title,
156 Authority $performer
157 ): void {
158 $isUserJsConfig = $title->isUserJsConfigPage();
159 $namespace = $title->getNamespace();
160 $intro = '';
161
162 if (
163 $title->isUserConfigPage() &&
164 $title->isSubpageOf( Title::makeTitle( NS_USER, $performer->getUser()->getName() ) )
165 ) {
166 $isUserCssConfig = $title->isUserCssConfigPage();
167 $isUserJsonConfig = $title->isUserJsonConfigPage();
168 $isUserJsConfig = $title->isUserJsConfigPage();
169
170 if ( $isUserCssConfig ) {
171 $warning = 'usercssispublic';
172 } elseif ( $isUserJsonConfig ) {
173 $warning = 'userjsonispublic';
174 } else {
175 $warning = 'userjsispublic';
176 }
177
178 $warningText = $localizer->msg( $warning )->parse();
179 $intro .= $warningText ? Html::rawElement(
180 'div',
181 [ 'class' => 'mw-userconfigpublic' ],
182 $warningText
183 ) : '';
184
185 }
186 $codeMsg = $localizer->msg( 'editpage-code-message' );
187 $codeMessageText = $codeMsg->isDisabled() ? '' : $codeMsg->parseAsBlock();
188 $isJavaScript = $title->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ||
189 $title->hasContentModel( CONTENT_MODEL_VUE );
190 $isCSS = $title->hasContentModel( CONTENT_MODEL_CSS );
191
192 if ( $namespace === NS_MEDIAWIKI ) {
193 $interfaceMsg = $localizer->msg( 'editinginterface' );
194 if ( $isJavaScript ) {
195 $interfaceMsg = $localizer->msg( 'editinginterfacejs' );
196 }
197 $interfaceMsgText = $interfaceMsg->parse();
198 # Show a warning if editing an interface message
199 $intro .= $interfaceMsgText ? Html::rawElement(
200 'div',
201 [ 'class' => 'mw-editinginterface' ],
202 $interfaceMsgText
203 ) : '';
204 # If this is a default message (but not css, json, js or vue),
205 # show a hint that it is translatable on translatewiki.net
206 if (
207 !$isCSS
208 && !$title->hasContentModel( CONTENT_MODEL_JSON )
209 && !$isJavaScript
210 && $this->shadowPageLoader->getMessageProvider()->existsForLink( $title )
211 ) {
212 $translateInterfaceText = $localizer->msg( 'translateinterface' )->parse();
213 $intro .= $translateInterfaceText ? Html::rawElement(
214 'div',
215 [ 'class' => 'mw-translateinterface' ],
216 $translateInterfaceText
217 ) : '';
218 }
219 }
220
221 if ( $isUserJsConfig ) {
222 $userConfigDangerousMsg = $localizer->msg( 'userjsdangerous' )->parse();
223 $intro .= $userConfigDangerousMsg ? Html::rawElement(
224 'div',
225 [ 'class' => 'mw-userconfigdangerous' ],
226 $userConfigDangerousMsg
227 ) : '';
228 }
229
230 // If the wiki page contains JavaScript or CSS link add message specific to code.
231 if ( $isJavaScript || $isCSS ) {
232 $intro .= $codeMessageText;
233 }
234
235 $messages->addWithKey(
236 'code-editing-intro',
237 $intro,
238 // While semantically this is a warning, given the impact of editing these pages,
239 // it's best to deter users who don't understand what they are doing by
240 // acknowledging the danger here. This is a potentially destructive action
241 // so requires destructive coloring.
242 Html::warningBox( '$1' )
243 );
244 }
245
246 private function addSharedRepoHint(
247 IntroMessageList $messages,
248 MessageLocalizer $localizer,
249 ProperPageIdentity $page
250 ): void {
251 $namespace = $page->getNamespace();
252 if ( $namespace === NS_FILE ) {
253 # Show a hint to shared repo
254 $file = $this->repoGroup->findFile( $page );
255 if ( $file && !$file->isLocal() ) {
256 $descUrl = $file->getDescriptionUrl();
257 # there must be a description url to show a hint to shared repo
258 if ( $descUrl ) {
259 if ( !$page->exists() ) {
260 $messages->add(
261 $localizer->msg(
262 'sharedupload-desc-create',
263 $file->getRepo()->getDisplayName(),
264 $descUrl
265 ),
266 "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>"
267 );
268 } else {
269 $messages->add(
270 $localizer->msg(
271 'sharedupload-desc-edit',
272 $file->getRepo()->getDisplayName(),
273 $descUrl
274 ),
275 "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>"
276 );
277 }
278 }
279 }
280 }
281 }
282
283 private function addUserWarnings(
284 IntroMessageList $messages,
285 MessageLocalizer $localizer,
286 Title $title,
287 Authority $performer
288 ): void {
289 $namespace = $title->getNamespace();
290 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
291 # Show log extract when the user is currently blocked
292 if ( $namespace === NS_USER || $namespace === NS_USER_TALK ) {
293 $username = explode( '/', $title->getText(), 2 )[0];
294 // Allow IP users
295 $validation = UserRigorOptions::RIGOR_NONE;
296 $user = $this->userFactory->newFromName( $username, $validation );
297 $ip = $this->userNameUtils->isIP( $username );
298
299 $userExists = ( $user && $user->isRegistered() );
300 if ( $userExists && $user->isHidden() && !$performer->isAllowed( 'hideuser' ) ) {
301 // If the user exists, but is hidden, and the viewer cannot see hidden
302 // users, pretend like they don't exist at all. See T120883
303 $userExists = false;
304 }
305
306 if ( !$userExists && !$ip ) {
307 $messages->addWithKey(
308 'userpage-userdoesnotexist',
309 // This wrapper frame, for whatever reason, is not optional
310 Html::warningBox(
311 $localizer->msg( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) )->parse(),
312 'mw-userpage-userdoesnotexist'
313 )
314 );
315 return;
316 }
317
318 $blockLogBox = LogEventsList::getBlockLogWarningBox(
319 $this->blockStore,
320 $this->namespaceInfo,
321 $localizer,
322 $this->linkRenderer,
323 $user,
324 $title
325 );
326 if ( $blockLogBox !== null ) {
327 $messages->addWithKey( 'blocked-notice-logextract', $blockLogBox );
328 }
329 }
330 }
331
335 private function addEditIntro(
336 IntroMessageList $messages,
337 MessageLocalizer $localizer,
338 ProperPageIdentity $page,
339 Authority $performer,
340 ?string $editIntro,
341 ?string $section
342 ): void {
343 if ( ( $editIntro === null || $editIntro === '' ) && $section === 'new' ) {
344 // Custom edit intro for new sections
345 $editIntro = 'MediaWiki:addsection-editintro';
346 }
347 if ( $editIntro !== null && $editIntro !== '' ) {
348 $introTitle = Title::newFromText( $editIntro );
349
350 // (T334855) Use SpecialMyLanguage redirect so that nonexistent translated pages can
351 // fall back to the corresponding page in a suitable language
352 $introTitle = $this->getTargetTitleIfSpecialMyLanguage( $introTitle );
353
354 if ( $this->isPageExistingAndViewable( $introTitle, $performer ) ) {
355 $messages->addWithKey(
356 'editintro',
357 $localizer->msg( new RawMessage(
358 // Added using template syntax, to take <noinclude>'s into account.
359 '<div class="mw-editintro">{{:' . $introTitle->getFullText() . '}}</div>'
360 ) )
361 // Parse as content to enable language conversion (T353870)
362 ->inContentLanguage()
363 ->parse()
364 );
365 return;
366 }
367 }
368
369 if ( !$page->exists() ) {
370 $helpLink = $this->urlUtils->expand(
371 Skin::makeInternalOrExternalUrl(
372 $localizer->msg( 'helppage' )->inContentLanguage()->text()
373 ),
375 );
376 if ( $helpLink === null ) {
377 throw new LogicException( 'Help link was invalid, this should be impossible' );
378 }
379 if ( $performer->getUser()->isRegistered() ) {
380 $messages->add(
381 $localizer->msg( 'newarticletext', $helpLink ),
382 // Suppress the external link icon, consider the help url an internal one
383 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>"
384 );
385 } else {
386 $messages->add(
387 $localizer->msg( 'newarticletextanon', $helpLink ),
388 // Suppress the external link icon, consider the help url an internal one
389 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>"
390 );
391 }
392 }
393 }
394
395 private function addRecreateWarning(
396 IntroMessageList $messages,
397 ProperPageIdentity $page
398 ): void {
399 # Give a notice if the user is editing a deleted/moved page...
400 if ( !$page->exists() ) {
401 $dbr = $this->dbProvider->getReplicaDatabase();
402
403 $messages->addWithKey(
404 'recreate-moveddeleted-warn',
405 $this->getLogExtract( [ 'delete', 'move', 'merge' ], $page, '', [
406 'lim' => 10,
407 'conds' => [ $dbr->expr( 'log_action', '!=', 'revision' ) ],
408 'showIfEmpty' => false,
409 'msgKey' => [ 'recreate-moveddeleted-warn' ],
410 ] )
411 );
412 }
413 }
414
415 private function addTalkPageText(
416 IntroMessageList $messages,
417 MessageLocalizer $localizer,
418 Title $title
419 ): void {
420 if ( $title->isTalkPage() ) {
421 $messages->add( $localizer->msg( 'talkpagetext' ) );
422 }
423 }
424
425 private function addEditNotices(
426 IntroMessageList $messages,
427 MessageLocalizer $localizer,
428 Title $title,
429 ?RevisionRecord $revRecord
430 ): void {
431 $editNotices = $title->getEditNotices( $revRecord ? $revRecord->getId() : 0 );
432 if ( count( $editNotices ) ) {
433 foreach ( $editNotices as $key => $html ) {
434 $messages->addWithKey( $key, $html );
435 }
436 } else {
437 $msg = $localizer->msg( 'editnotice-notext' );
438 if ( !$msg->isDisabled() ) {
439 $messages->addWithKey(
440 'editnotice-notext',
441 Html::rawElement(
442 'div',
443 [ 'class' => 'mw-editnotice-notext' ],
444 $msg->parseAsBlock()
445 )
446 );
447 }
448 }
449 }
450
451 private function addOldRevisionWarning(
452 IntroMessageList $messages,
453 MessageLocalizer $localizer,
454 ?RevisionRecord $revRecord
455 ): void {
456 if ( $revRecord && !$revRecord->isCurrent() ) {
457 // This wrapper frame is not optional (T337071)
458 $messages->addWithKey( 'editingold', Html::warningBox( $localizer->msg( 'editingold' )->parse() ) );
459 }
460 }
461
462 private function addReadOnlyWarning(
463 IntroMessageList $messages,
464 MessageLocalizer $localizer
465 ): void {
466 if ( $this->readOnlyMode->isReadOnly() ) {
467 $messages->add(
468 $localizer->msg( 'readonlywarning', $this->readOnlyMode->getReason() ),
469 "<div id=\"mw-read-only-warning\">\n$1\n</div>"
470 );
471 }
472 }
473
474 private function addAnonEditWarning(
475 IntroMessageList $messages,
476 MessageLocalizer $localizer,
477 Title $title,
478 Authority $performer,
479 ?string $returnToQuery,
480 bool $preview
481 ): void {
482 if ( !$performer->getUser()->isRegistered() ) {
483 $tempUserCreateActive = $this->tempUserCreator->shouldAutoCreate( $performer, 'edit' );
484 if ( !$preview ) {
485 $messages->addWithKey(
486 'anoneditwarning',
487 $localizer->msg(
488 $tempUserCreateActive ? 'autocreate-edit-warning' : 'anoneditwarning',
489 // Log-in link
490 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
491 'returnto' => $title->getPrefixedDBkey(),
492 'returntoquery' => $returnToQuery,
493 ] ),
494 // Sign-up link
495 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
496 'returnto' => $title->getPrefixedDBkey(),
497 'returntoquery' => $returnToQuery,
498 ] )
499 )->parse(),
500 Html::warningBox( '$1', 'mw-anon-edit-warning' )
501 );
502 } else {
503 $messages->addWithKey(
504 'anoneditwarning',
505 $localizer->msg( $tempUserCreateActive ? 'autocreate-preview-warning' : 'anonpreviewwarning' )
506 ->parse(),
507 Html::warningBox( '$1', 'mw-anon-preview-warning' ) );
508 }
509 }
510 }
511
516 private function isWrongCaseUserConfigPage( Title $title ): bool {
517 if ( $title->isUserCssConfigPage() || $title->isUserJsConfigPage() ) {
518 $name = $title->getSkinFromConfigSubpage();
519 $skins = array_merge(
520 array_keys( $this->skinFactory->getInstalledSkins() ),
521 [ 'common' ]
522 );
523 return !in_array( $name, $skins, true )
524 && in_array( strtolower( $name ), $skins, true );
525 } else {
526 return false;
527 }
528 }
529
530 private function addUserConfigPageInfo(
531 IntroMessageList $messages,
532 MessageLocalizer $localizer,
533 Title $title,
534 Authority $performer,
535 bool $preview
536 ): void {
537 if ( $title->isUserConfigPage() ) {
538 # Check the skin exists
539 if ( $this->isWrongCaseUserConfigPage( $title ) ) {
540 $messages->add(
541 $localizer->msg( 'userinvalidconfigtitle', $title->getSkinFromConfigSubpage() ),
542 Html::errorBox( '$1', '', 'mw-userinvalidconfigtitle' )
543 );
544 }
545 if ( $title->isSubpageOf( Title::makeTitle( NS_USER, $performer->getUser()->getName() ) ) ) {
546 $isUserCssConfig = $title->isUserCssConfigPage();
547 $isUserJsonConfig = $title->isUserJsonConfigPage();
548 $isUserJsConfig = $title->isUserJsConfigPage();
549
550 if ( !$preview ) {
551 if ( $isUserCssConfig && $this->config->get( MainConfigNames::AllowUserCss ) ) {
552 $messages->add(
553 $localizer->msg( 'usercssyoucanpreview' ),
554 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>"
555 );
556 } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
557 $messages->add(
558 $localizer->msg( 'userjsonyoucanpreview' ),
559 "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>"
560 );
561 } elseif ( $isUserJsConfig && $this->config->get( MainConfigNames::AllowUserJs ) ) {
562 $messages->add(
563 $localizer->msg( 'userjsyoucanpreview' ),
564 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>"
565 );
566 }
567 }
568 }
569 }
570 }
571
572 private function addPageProtectionWarningHeaders(
573 IntroMessageList $messages,
574 MessageLocalizer $localizer,
575 ProperPageIdentity $page
576 ): void {
577 if ( $this->restrictionStore->isProtected( $page, 'edit' ) &&
578 $this->permManager->getNamespaceRestrictionLevels(
579 $page->getNamespace()
580 ) !== [ '' ]
581 ) {
582 # Is the title semi-protected?
583 if ( $this->restrictionStore->isSemiProtected( $page ) ) {
584 $noticeMsg = 'semiprotectedpagewarning';
585 } else {
586 # Then it must be protected based on static groups (regular)
587 $noticeMsg = 'protectedpagewarning';
588 }
589 $messages->addWithKey(
590 $noticeMsg,
591 $this->getLogExtract( 'protect', $page, '', [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] )
592 );
593 }
594 if ( $this->restrictionStore->isCascadeProtected( $page ) ) {
595 # Is this page under cascading protection from some source pages?
596 $tlCascadeSources = $this->restrictionStore->getCascadeProtectionSources( $page )[2];
597 if ( $tlCascadeSources ) {
598 $htmlList = '';
599 # Explain, and list the titles responsible
600 foreach ( $tlCascadeSources as $source ) {
601 $htmlList .= Html::rawElement( 'li', [], $this->linkRenderer->makeLink( $source ) );
602 }
603 $messages->addWithKey(
604 'cascadeprotectedwarning',
605 $localizer->msg( 'cascadeprotectedwarning', count( $tlCascadeSources ) )->parse() .
606 ( $htmlList ? Html::rawElement( 'ul', [], $htmlList ) : '' ),
607 Html::warningBox( '$1', 'mw-cascadeprotectedwarning' )
608 );
609 }
610 }
611 if ( !$page->exists() && $this->restrictionStore->getRestrictions( $page, 'create' ) ) {
612 $messages->addWithKey(
613 'titleprotectedwarning',
614 $this->getLogExtract(
615 'protect', $page,
616 '',
617 [
618 'lim' => 1,
619 'showIfEmpty' => false,
620 'msgKey' => [ 'titleprotectedwarning' ],
621 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>"
622 ]
623 )
624 );
625 }
626 }
627
628 private function addHeaderCopyrightWarning(
629 IntroMessageList $messages,
630 MessageLocalizer $localizer
631 ): void {
632 $messages->add(
633 $localizer->msg( 'editpage-head-copy-warn' ),
634 "<div class='editpage-head-copywarn'>\n$1\n</div>"
635 );
636 }
637}
const CONTENT_MODEL_VUE
Definition Defines.php:241
const NS_USER
Definition Defines.php:53
const CONTENT_MODEL_CSS
Definition Defines.php:237
const NS_FILE
Definition Defines.php:57
const PROTO_CURRENT
Definition Defines.php:222
const NS_MEDIAWIKI
Definition Defines.php:59
const CONTENT_MODEL_JSON
Definition Defines.php:239
const NS_USER_TALK
Definition Defines.php:54
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:236
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Provides the intro messages (edit notices and others) to be displayed before an edit form.
__construct(private readonly Config $config, private readonly LinkRenderer $linkRenderer, private readonly PermissionManager $permManager, private readonly UserNameUtils $userNameUtils, private readonly TempUserCreator $tempUserCreator, private readonly UserFactory $userFactory, private readonly RestrictionStore $restrictionStore, private readonly DatabaseBlockStore $blockStore, private readonly ReadOnlyMode $readOnlyMode, private readonly SpecialPageFactory $specialPageFactory, private readonly RepoGroup $repoGroup, private readonly NamespaceInfo $namespaceInfo, private readonly SkinFactory $skinFactory, private readonly IConnectionProvider $dbProvider, private readonly UrlUtils $urlUtils, private readonly ShadowPageLoader $shadowPageLoader,)
getIntroMessages(int $frames, array $skip, MessageLocalizer $localizer, ProperPageIdentity $page, ?RevisionRecord $revRecord, Authority $performer, ?string $editIntro, ?string $returnToQuery, bool $preview, ?string $section=null)
Return intro messages to be shown before an edit form.
Encapsulates a list of edit form intro messages (as HTML) with their identifiers.
Prioritized list of file repositories.
Definition RepoGroup.php:30
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Variant of the Message class.
Class that generates HTML for internal links.
A class containing constants representing the names of configuration variables.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Page revision base class.
A service which loads shadow content, which is content that is displayed on a nonexistent page with a...
Factory class to create Skin objects.
The base class for all skins.
Definition Skin.php:54
Factory for handling the special page list and generating SpecialPage objects.
Parent class for all special pages.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:69
Service for temporary user creation.
Create User objects.
UserNameUtils service.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Determine whether a site is currently in read-only mode.
Interface for configuration instances.
Definition Config.php:18
Interface for localizing messages in MediaWiki.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Interface for a page that is (or could be, or used to be) an editable wiki page.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
Shared interface for rigor levels when dealing with User methods.
Provide primary and replica IDatabase connections.
$source
msg( $key,... $params)