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