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