wiki.techinc.nl/includes/editpage/IntroMessageBuilder.php
Bartosz Dziewoński 6a20dc29ae editpage: Split off producing edit intro messages and preloaded content
Introduce two new classes, containing code split off from EditPage:
* IntroMessageBuilder (edit notices and other intro messages)
* PreloadedContentBuilder (initial text of new pages / sections)

I'm doing both of these features in one change, because they share a
lot of code. They are meant to be used by alternative editors to
support all of the features of the MediaWiki edit form. This isn't
everything you need yet (we should at least do this for the edit
checkboxes too), but it's a step.

Bug: T201613
Change-Id: If0b05710cb52a977bf4e85947d72d68683a0a29e
2023-05-16 20:51:00 +02:00

652 lines
20 KiB
PHP

<?php
namespace MediaWiki\EditPage;
use Config;
use LogEventsList;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Html\Html;
use MediaWiki\Language\RawMessage;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MainConfigNames;
use MediaWiki\Page\ProperPageIdentity;
use MediaWiki\Permissions\Authority;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Permissions\RestrictionStore;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\Title\Title;
use MediaWiki\User\TempUser\TempUserCreator;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserRigorOptions;
use MessageLocalizer;
use NamespaceInfo;
use RepoGroup;
use Skin;
use SkinFactory;
use SpecialPage;
use Wikimedia\Rdbms\ReadOnlyMode;
/**
* Provides the intro messages (edit notices and others) to be displayed before an edit form.
*
* Used by EditPage, and may be used by extensions providing alternative editors.
*
* @since 1.41
*/
class IntroMessageBuilder {
use ParametersHelper;
// Parameters for getIntroMessages()
public const MORE_FRAMES = 1;
public const LESS_FRAMES = 2;
private Config $config;
private LinkRenderer $linkRenderer;
private PermissionManager $permManager;
private UserNameUtils $userNameUtils;
private TempUserCreator $tempUserCreator;
private UserFactory $userFactory;
private RestrictionStore $restrictionStore;
private ReadOnlyMode $readOnlyMode;
private SpecialPageFactory $specialPageFactory;
private RepoGroup $repoGroup;
private NamespaceInfo $namespaceInfo;
private SkinFactory $skinFactory;
public function __construct(
Config $config,
LinkRenderer $linkRenderer,
PermissionManager $permManager,
UserNameUtils $userNameUtils,
TempUserCreator $tempUserCreator,
UserFactory $userFactory,
RestrictionStore $restrictionStore,
ReadOnlyMode $readOnlyMode,
SpecialPageFactory $specialPageFactory,
RepoGroup $repoGroup,
NamespaceInfo $namespaceInfo,
SkinFactory $skinFactory
) {
$this->config = $config;
$this->linkRenderer = $linkRenderer;
$this->permManager = $permManager;
$this->userNameUtils = $userNameUtils;
$this->tempUserCreator = $tempUserCreator;
$this->userFactory = $userFactory;
$this->restrictionStore = $restrictionStore;
$this->readOnlyMode = $readOnlyMode;
$this->specialPageFactory = $specialPageFactory;
$this->repoGroup = $repoGroup;
$this->namespaceInfo = $namespaceInfo;
$this->skinFactory = $skinFactory;
}
/**
* Wrapper for LogEventsList::showLogExtract() that returns the string with the output.
*
* LogEventsList::showLogExtract() has some side effects affecting the global state (main request
* context), which should not be relied upon.
*
* @param string|array $types See LogEventsList::showLogExtract()
* @param string $page See LogEventsList::showLogExtract()
* @param string $user See LogEventsList::showLogExtract()
* @param array $param See LogEventsList::showLogExtract()
* @return string
*/
private function getLogExtract( $types = [], $page = '', $user = '', $param = [] ): string {
$outString = '';
LogEventsList::showLogExtract( $outString, $types, $page, $user, $param );
return $outString;
}
/**
* Return intro messages to be shown before an edit form.
*
* The message identifiers used as array keys are stable. Callers of this method may recognize
* specific messages and omit them when displaying, if they're not applicable to some interface or
* if they provide the same information in an alternative way.
*
* Callers should load the 'mediawiki.interface.helpers.styles' ResourceLoader module, as some of
* the possible messages rely on those styles.
*
* @param int $frames Some intro messages come with optional wrapper frames.
* Pass IntroMessageBuilder::MORE_FRAMES to include the frames whenever possible,
* or IntroMessageBuilder::LESS_FRAMES to omit them whenever possible.
* @param string[] $skip Identifiers of messages not to generate
* @param MessageLocalizer $localizer
* @param ProperPageIdentity $page Page being viewed
* @param RevisionRecord|null $revRecord Revision being viewed, null if page doesn't exist
* @param Authority $performer
* @param string|null $editIntro
* @param string|null $returnToQuery
* @param bool $preview
* @return array<string,string> Ordered map of identifiers to message HTML
*/
public function getIntroMessages(
int $frames,
array $skip,
MessageLocalizer $localizer,
ProperPageIdentity $page,
?RevisionRecord $revRecord,
Authority $performer,
?string $editIntro,
?string $returnToQuery,
bool $preview
): array {
$title = Title::newFromPageIdentity( $page );
$messages = new IntroMessageList( $frames, $skip );
if ( !$preview ) {
$this->addCodeEditingIntro( $messages, $localizer, $title, $performer );
$this->addSharedRepoHint( $messages, $localizer, $page );
$this->addUserWarnings( $messages, $localizer, $title, $performer );
$this->addEditIntro( $messages, $localizer, $page, $performer, $editIntro );
$this->addRecreateWarning( $messages, $localizer, $page );
}
$this->addTalkPageText( $messages, $localizer, $title );
$this->addEditNotices( $messages, $localizer, $title, $revRecord );
$this->addOldRevisionWarning( $messages, $localizer, $revRecord );
$this->addReadOnlyWarning( $messages, $localizer );
$this->addAnonEditWarning( $messages, $localizer, $title, $performer, $returnToQuery, $preview );
$this->addUserConfigPageInfo( $messages, $localizer, $title, $performer, $preview );
$this->addPageProtectionWarningHeaders( $messages, $localizer, $page );
$this->addHeaderCopyrightWarning( $messages, $localizer );
return $messages->getList();
}
/**
* Adds introduction to code editing.
*/
private function addCodeEditingIntro(
IntroMessageList $messages,
MessageLocalizer $localizer,
Title $title,
Authority $performer
): void {
$isUserJsConfig = $title->isUserJsConfigPage();
$namespace = $title->getNamespace();
$intro = '';
if (
$title->isUserConfigPage() &&
$title->isSubpageOf( Title::makeTitle( NS_USER, $performer->getUser()->getName() ) )
) {
$isUserCssConfig = $title->isUserCssConfigPage();
$isUserJsonConfig = $title->isUserJsonConfigPage();
$isUserJsConfig = $title->isUserJsConfigPage();
if ( $isUserCssConfig ) {
$warning = 'usercssispublic';
} elseif ( $isUserJsonConfig ) {
$warning = 'userjsonispublic';
} else {
$warning = 'userjsispublic';
}
$warningText = $localizer->msg( $warning )->parse();
$intro .= $warningText ? Html::rawElement(
'div',
[ 'class' => 'mw-userconfigpublic' ],
$warningText
) : '';
}
$codeMsg = $localizer->msg( 'editpage-code-message' );
$codeMessageText = $codeMsg->isDisabled() ? '' : $codeMsg->parseAsBlock();
$isJavaScript = $title->hasContentModel( CONTENT_MODEL_JAVASCRIPT );
$isCSS = $title->hasContentModel( CONTENT_MODEL_CSS );
if ( $namespace === NS_MEDIAWIKI ) {
$interfaceMsg = $localizer->msg( 'editinginterface' );
$interfaceMsgText = $interfaceMsg->parse();
# Show a warning if editing an interface message
$intro .= $interfaceMsgText ? Html::rawElement(
'div',
[ 'class' => 'mw-editinginterface' ],
$interfaceMsgText
) : '';
# If this is a default message (but not css, json, or js),
# show a hint that it is translatable on translatewiki.net
if (
!$isCSS
&& !$title->hasContentModel( CONTENT_MODEL_JSON )
&& !$isJavaScript
) {
$defaultMessageText = $title->getDefaultMessageText();
if ( $defaultMessageText !== false ) {
$translateInterfaceText = $localizer->msg( 'translateinterface' )->parse();
$intro .= $translateInterfaceText ? Html::rawElement(
'div',
[ 'class' => 'mw-translateinterface' ],
$translateInterfaceText
) : '';
}
}
}
if ( $isUserJsConfig ) {
$userConfigDangerousMsg = $localizer->msg( 'userjsdangerous' )->parse();
$intro .= $userConfigDangerousMsg ? Html::rawElement(
'div',
[ 'class' => 'mw-userconfigdangerous' ],
$userConfigDangerousMsg
) : '';
}
// If the wiki page contains JavaScript or CSS link add message specific to code.
if ( $isJavaScript || $isCSS ) {
$intro .= $codeMessageText;
}
$messages->addWithKey(
'code-editing-intro',
$intro,
// While semantically this is a warning, given the impact of editing these pages,
// it's best to deter users who don't understand what they are doing by
// acknowledging the danger here. This is a potentially destructive action
// so requires destructive coloring.
Html::errorBox( '$1' )
);
}
private function addSharedRepoHint(
IntroMessageList $messages,
MessageLocalizer $localizer,
ProperPageIdentity $page
): void {
$namespace = $page->getNamespace();
if ( $namespace === NS_FILE ) {
# Show a hint to shared repo
$file = $this->repoGroup->findFile( $page );
if ( $file && !$file->isLocal() ) {
$descUrl = $file->getDescriptionUrl();
# there must be a description url to show a hint to shared repo
if ( $descUrl ) {
if ( !$page->exists() ) {
$messages->add(
$localizer->msg(
'sharedupload-desc-create',
$file->getRepo()->getDisplayName(),
$descUrl
),
"<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>"
);
} else {
$messages->add(
$localizer->msg(
'sharedupload-desc-edit',
$file->getRepo()->getDisplayName(),
$descUrl
),
"<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>"
);
}
}
}
}
}
private function addUserWarnings(
IntroMessageList $messages,
MessageLocalizer $localizer,
Title $title,
Authority $performer
): void {
$namespace = $title->getNamespace();
# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
# Show log extract when the user is currently blocked
if ( $namespace === NS_USER || $namespace === NS_USER_TALK ) {
$username = explode( '/', $title->getText(), 2 )[0];
// Allow IP users
$validation = UserRigorOptions::RIGOR_NONE;
$user = $this->userFactory->newFromName( $username, $validation );
$ip = $this->userNameUtils->isIP( $username );
$block = DatabaseBlock::newFromTarget( $user, $user );
$userExists = ( $user && $user->isRegistered() );
if ( $userExists && $user->isHidden() && !$performer->isAllowed( 'hideuser' ) ) {
// If the user exists, but is hidden, and the viewer cannot see hidden
// users, pretend like they don't exist at all. See T120883
$userExists = false;
}
if ( !$userExists && !$ip ) {
$messages->addWithKey(
'userpage-userdoesnotexist',
// This wrapper frame, for whatever reason, is not optional
Html::warningBox(
$localizer->msg( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) )->parse(),
'mw-userpage-userdoesnotexist'
)
);
} elseif (
$block !== null &&
$block->getType() !== DatabaseBlock::TYPE_AUTO &&
(
$block->isSitewide() ||
$this->permManager->isBlockedFrom(
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
$user,
$title,
true
)
)
) {
// Show log extract if the user is sitewide blocked or is partially
// blocked and not allowed to edit their user page or user talk page
$messages->addWithKey(
'blocked-notice-logextract',
$this->getLogExtract(
'block',
$this->namespaceInfo->getCanonicalName( NS_USER ) . ':' . $block->getTargetName(),
'',
[
'lim' => 1,
'showIfEmpty' => false,
'msgKey' => [
'blocked-notice-logextract',
$user->getName() # Support GENDER in notice
],
]
)
);
}
}
}
/**
* Try to add a custom edit intro, or use the standard one if this is not possible.
*/
private function addEditIntro(
IntroMessageList $messages,
MessageLocalizer $localizer,
ProperPageIdentity $page,
Authority $performer,
?string $editIntro
): void {
if ( $editIntro ) {
$introTitle = Title::newFromText( $editIntro );
// (T334855) Use SpecialMyLanguage redirect so that nonexistent translated pages can
// fall back to the corresponding page in a suitable language
$introTitle = $this->getTargetTitleIfSpecialMyLanguage( $introTitle );
if ( $this->isPageExistingAndViewable( $introTitle, $performer ) ) {
$messages->addWithKey(
'editintro',
$localizer->msg( new RawMessage(
// Added using template syntax, to take <noinclude>'s into account.
'<div class="mw-editintro">{{:' . $introTitle->getFullText() . '}}</div>'
) )->parse()
);
return;
}
}
if ( !$page->exists() ) {
$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
$localizer->msg( 'helppage' )->inContentLanguage()->text()
) );
if ( $performer->getUser()->isRegistered() ) {
$messages->add(
$localizer->msg( 'newarticletext', $helpLink ),
// Suppress the external link icon, consider the help url an internal one
"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>"
);
} else {
$messages->add(
$localizer->msg( 'newarticletextanon', $helpLink ),
// Suppress the external link icon, consider the help url an internal one
"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>"
);
}
}
}
private function addRecreateWarning(
IntroMessageList $messages,
MessageLocalizer $localizer,
ProperPageIdentity $page
): void {
# Give a notice if the user is editing a deleted/moved page...
if ( !$page->exists() ) {
$dbr = wfGetDB( DB_REPLICA );
$messages->addWithKey(
'recreate-moveddeleted-warn',
$this->getLogExtract( [ 'delete', 'move' ], $page, '', [
'lim' => 10,
'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
'showIfEmpty' => false,
'msgKey' => [ 'recreate-moveddeleted-warn' ],
] )
);
}
}
private function addTalkPageText(
IntroMessageList $messages,
MessageLocalizer $localizer,
Title $title
): void {
if ( $title->isTalkPage() ) {
$messages->add( $localizer->msg( 'talkpagetext' ) );
}
}
private function addEditNotices(
IntroMessageList $messages,
MessageLocalizer $localizer,
Title $title,
?RevisionRecord $revRecord
): void {
$editNotices = $title->getEditNotices( $revRecord ? $revRecord->getId() : 0 );
if ( count( $editNotices ) ) {
foreach ( $editNotices as $key => $html ) {
$messages->addWithKey( $key, $html );
}
} else {
$msg = $localizer->msg( 'editnotice-notext' );
if ( !$msg->isDisabled() ) {
$messages->addWithKey(
'editnotice-notext',
Html::rawElement(
'div',
[ 'class' => 'mw-editnotice-notext' ],
$msg->parseAsBlock()
)
);
}
}
}
private function addOldRevisionWarning(
IntroMessageList $messages,
MessageLocalizer $localizer,
?RevisionRecord $revRecord
): void {
if ( $revRecord && !$revRecord->isCurrent() ) {
$messages->add( $localizer->msg( 'editingold' ), Html::warningBox( "\n$1\n" ) );
}
}
private function addReadOnlyWarning(
IntroMessageList $messages,
MessageLocalizer $localizer
): void {
if ( $this->readOnlyMode->isReadOnly() ) {
$messages->add(
$localizer->msg( 'readonlywarning', $this->readOnlyMode->getReason() ),
"<div id=\"mw-read-only-warning\">\n$1\n</div>"
);
}
}
private function addAnonEditWarning(
IntroMessageList $messages,
MessageLocalizer $localizer,
Title $title,
Authority $performer,
?string $returnToQuery,
bool $preview
): void {
if ( !$performer->getUser()->isRegistered() ) {
$tempUserCreateActive = $this->tempUserCreator->shouldAutoCreate( $performer, 'edit' );
if ( !$preview ) {
$messages->addWithKey(
'anoneditwarning',
$localizer->msg(
$tempUserCreateActive ? 'autocreate-edit-warning' : 'anoneditwarning',
// Log-in link
SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
'returnto' => $title->getPrefixedDBkey(),
'returntoquery' => $returnToQuery,
] ),
// Sign-up link
SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
'returnto' => $title->getPrefixedDBkey(),
'returntoquery' => $returnToQuery,
] )
)->parse(),
Html::warningBox( '$1', 'mw-anon-edit-warning' )
);
} else {
$messages->addWithKey(
'anoneditwarning',
$localizer->msg( $tempUserCreateActive ? 'autocreate-preview-warning' : 'anonpreviewwarning' )
->parse(),
Html::warningBox( '$1', 'mw-anon-preview-warning' ) );
}
}
}
/**
* Checks whether the user entered a skin name in uppercase,
* e.g. "User:Example/Monobook.css" instead of "monobook.css"
*/
private function isWrongCaseUserConfigPage( Title $title ): bool {
if ( $title->isUserConfigPage() ) {
$name = $title->getSkinFromConfigSubpage();
$skins = array_merge(
array_keys( $this->skinFactory->getInstalledSkins() ),
[ 'common' ]
);
return !in_array( $name, $skins, true )
&& in_array( strtolower( $name ), $skins, true );
} else {
return false;
}
}
private function addUserConfigPageInfo(
IntroMessageList $messages,
MessageLocalizer $localizer,
Title $title,
Authority $performer,
bool $preview
): void {
if ( $title->isUserConfigPage() ) {
# Check the skin exists
if ( $this->isWrongCaseUserConfigPage( $title ) ) {
$messages->add(
$localizer->msg( 'userinvalidconfigtitle', $title->getSkinFromConfigSubpage() ),
Html::errorBox( '$1', '', 'mw-userinvalidconfigtitle' )
);
}
if ( $title->isSubpageOf( Title::makeTitle( NS_USER, $performer->getUser()->getName() ) ) ) {
$isUserCssConfig = $title->isUserCssConfigPage();
$isUserJsonConfig = $title->isUserJsonConfigPage();
$isUserJsConfig = $title->isUserJsConfigPage();
if ( !$preview ) {
if ( $isUserCssConfig && $this->config->get( MainConfigNames::AllowUserCss ) ) {
$messages->add(
$localizer->msg( 'usercssyoucanpreview' ),
"<div id='mw-usercssyoucanpreview'>\n$1\n</div>"
);
} elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
$messages->add(
$localizer->msg( 'userjsonyoucanpreview' ),
"<div id='mw-userjsonyoucanpreview'>\n$1\n</div>"
);
} elseif ( $isUserJsConfig && $this->config->get( MainConfigNames::AllowUserJs ) ) {
$messages->add(
$localizer->msg( 'userjsyoucanpreview' ),
"<div id='mw-userjsyoucanpreview'>\n$1\n</div>"
);
}
}
}
}
}
private function addPageProtectionWarningHeaders(
IntroMessageList $messages,
MessageLocalizer $localizer,
ProperPageIdentity $page
): void {
if ( $this->restrictionStore->isProtected( $page, 'edit' ) &&
$this->permManager->getNamespaceRestrictionLevels(
$page->getNamespace()
) !== [ '' ]
) {
# Is the title semi-protected?
if ( $this->restrictionStore->isSemiProtected( $page ) ) {
$noticeMsg = 'semiprotectedpagewarning';
} else {
# Then it must be protected based on static groups (regular)
$noticeMsg = 'protectedpagewarning';
}
$messages->addWithKey(
$noticeMsg,
$this->getLogExtract( 'protect', $page, '', [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] )
);
}
if ( $this->restrictionStore->isCascadeProtected( $page ) ) {
# Is this page under cascading protection from some source pages?
$cascadeSources = $this->restrictionStore->getCascadeProtectionSources( $page )[0];
$htmlList = '';
# Explain, and list the titles responsible
foreach ( $cascadeSources as $source ) {
$htmlList .= Html::rawElement( 'li', [], $this->linkRenderer->makeLink( $source ) );
}
$messages->addWithKey(
'cascadeprotectedwarning',
$localizer->msg( 'cascadeprotectedwarning', count( $cascadeSources ) )->parse() .
( $htmlList ? Html::rawElement( 'ul', [], $htmlList ) : '' ),
Html::warningBox( '$1', 'mw-cascadeprotectedwarning' )
);
}
if ( !$page->exists() && $this->restrictionStore->getRestrictions( $page, 'create' ) ) {
$messages->addWithKey(
'titleprotectedwarning',
$this->getLogExtract(
'protect', $page,
'',
[
'lim' => 1,
'showIfEmpty' => false,
'msgKey' => [ 'titleprotectedwarning' ],
'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>"
]
)
);
}
}
private function addHeaderCopyrightWarning(
IntroMessageList $messages,
MessageLocalizer $localizer
): void {
$messages->add(
$localizer->msg( 'editpage-head-copy-warn' ),
"<div class='editpage-head-copywarn'>\n$1\n</div>"
);
}
}