MediaWiki master
UserLinkRenderer.php
Go to the documentation of this file.
1<?php
2declare( strict_types=1 );
3
4namespace MediaWiki\Linker;
5
23use Wikimedia\IPUtils;
25
32
33 private HookRunner $hookRunner;
34 private TempUserConfig $tempUserConfig;
35 private SpecialPageFactory $specialPageFactory;
36 private LinkRenderer $linkRenderer;
37 private TempUserDetailsLookup $tempUserDetailsLookup;
38 private UserIdentityLookup $userIdentityLookup;
39 private UserNameUtils $userNameUtils;
40
47 private MapCacheLRU $userLinkCache;
48
49 public function __construct(
50 HookContainer $hookContainer,
51 TempUserConfig $tempUserConfig,
52 SpecialPageFactory $specialPageFactory,
53 LinkRenderer $linkRenderer,
54 TempUserDetailsLookup $tempUserDetailsLookup,
55 UserIdentityLookup $userIdentityLookup,
56 UserNameUtils $userNameUtils
57 ) {
58 $this->hookRunner = new HookRunner( $hookContainer );
59 $this->tempUserConfig = $tempUserConfig;
60 $this->specialPageFactory = $specialPageFactory;
61 $this->linkRenderer = $linkRenderer;
62 $this->tempUserDetailsLookup = $tempUserDetailsLookup;
63 $this->userIdentityLookup = $userIdentityLookup;
64 $this->userNameUtils = $userNameUtils;
65
66 // Set a large enough cache size to accommodate long pagers,
67 // such as Special:RecentChanges with a high limit.
68 $this->userLinkCache = new MapCacheLRU( 1_000 );
69 }
70
83 public function userLink(
84 UserIdentity $targetUser,
85 IContextSource $context,
86 ?string $altUserName = null,
87 array $attributes = []
88 ): string {
89 $outputPage = $context->getOutput();
90 $outputPage->addModuleStyles( [
91 'mediawiki.interface.helpers.styles',
92 'mediawiki.interface.helpers.linker.styles'
93 ] );
94
95 $userName = $targetUser->getName();
96
97 if ( $this->isFromExternalWiki( $targetUser->getWikiId() ) ) {
98 $html = $this->userLinkCache->getWithSetCallback(
99 $this->userLinkCache->makeKey(
100 $targetUser->getWikiId(),
101 $userName,
102 $altUserName ?? '',
103 implode( ' ', $attributes )
104 ),
105 fn () => $this->renderExternalUserLink(
106 $targetUser,
107 $context,
108 $altUserName,
109 $attributes
110 )
111 );
112 } else {
113 $html = $this->userLinkCache->getWithSetCallback(
114 $this->userLinkCache->makeKey(
115 $userName,
116 $altUserName ?? '',
117 implode( ' ', $attributes )
118 ),
119 fn () => $this->renderUserLink(
120 $targetUser,
121 $context,
122 $altUserName,
123 $attributes
124 )
125 );
126 }
127 $prefix = '';
128 $postfix = '';
129 $this->hookRunner->onUserLinkRendererUserLinkPostRender( $targetUser, $context, $html, $prefix, $postfix );
130 return $prefix . $html . $postfix;
131 }
132
143 private function renderExternalUserLink(
144 UserIdentity $targetUser,
145 MessageLocalizer $messageLocalizer,
146 ?string $altUserName = null,
147 array $attributes = []
148 ): string {
149 $userName = $targetUser->getName();
150 $targetPage = Title::makeTitle( NS_USER, $userName );
151
152 $linkText = $altUserName ?? $userName;
153 $isDefaultCaption = $this->linkRenderer->isDefaultLinkCaption( $targetPage, $linkText );
154 $params = $this->getUserLinkParameters( $targetUser, $messageLocalizer, $isDefaultCaption );
155 $attributes += $params[ 'extraAttr' ];
156 $classes = $params[ 'classes' ];
157 $postfix = $params[ 'postfix' ];
158
159 $link = $this->linkRenderer->makeExternalLink(
160 WikiMap::getForeignURL(
161 $targetUser->getWikiId(),
162 'User:' . strtr( $userName, ' ', '_' )
163 ),
164 new HtmlArmor(
165 Html::element( 'bdi', [], $linkText ) . $postfix
166 ),
167 $targetPage,
168 '',
169 $attributes + [ 'class' => $classes ]
170 );
171
172 return $link;
173 }
174
190 private function renderUserLink(
191 UserIdentity $targetUser,
192 MessageLocalizer $messageLocalizer,
193 ?string $altUserName = null,
194 array $attributes = []
195 ): string {
196 $userName = $targetUser->getName();
197 $classes = [];
198
199 if ( $this->tempUserConfig->isTempName( $userName ) ) {
200 $pageName = $this->specialPageFactory->getLocalNameFor( 'Contributions', $userName );
201 $page = new TitleValue( NS_SPECIAL, $pageName );
202 } elseif ( !$targetUser->isRegistered() ) {
203 $page = ExternalUserNames::getUserLinkTitle( $userName );
204
205 if ( ExternalUserNames::isExternal( $userName ) ) {
206 $classes[] = 'mw-extuserlink';
207 } else {
208 $altUserName ??= IPUtils::prettifyIP( $userName );
209 }
210 $classes[] = 'mw-anonuserlink'; // Separate link class for anons (T45179)
211 } else {
212 $page = TitleValue::tryNew( NS_USER, strtr( $userName, ' ', '_' ) );
213 }
214
215 $linkText = $altUserName ?? $userName;
216 $isDefaultCaption = $this->linkRenderer->isDefaultLinkCaption( $page, $linkText );
217 $params = $this->getUserLinkParameters( $targetUser, $messageLocalizer, $isDefaultCaption );
218 $attributes += $params[ 'extraAttr' ];
219 $classes = array_merge( $params[ 'classes' ], $classes );
220 $postfix = $params[ 'postfix' ];
221
222 // Wrap the output with <bdi> tags for directionality isolation
223 $linkText =
224 '<bdi>' . htmlspecialchars( $linkText ) . '</bdi>'
225 . $postfix;
226
227 if ( isset( $attributes['class'] ) ) {
228 $classes[] = $attributes['class'];
229 }
230
231 $attributes['class'] = $classes;
232
233 if ( $page !== null ) {
234 return $this->linkRenderer->makeLink( $page, new HtmlArmor( $linkText ), $attributes );
235 }
236
237 return Html::rawElement( 'span', $attributes, $linkText );
238 }
239
246 private function getUserLinkParameters(
247 UserIdentity $targetUser,
248 MessageLocalizer $messageLocalizer,
249 bool $isDefaultCaption,
250 ) {
251 $attributes = [];
252 $userName = $targetUser->getName();
253 $isExpired = false;
254 $postfix = '';
255
256 $classes = $this->getLinkClassesFromUserName( $userName, $isDefaultCaption );
257 $classes[] = 'mw-userlink';
258 if ( $this->tempUserConfig->isTempName( $userName ) ) {
259 $attributes['data-mw-target'] = $userName;
260
261 if ( $this->isFromExternalWiki( $targetUser->getWikiId() ) ) {
262 // Check if the local wiki has an account with the same name and,
263 // if it does, check if it is expired. We can do this because
264 // temporary accounts expire on all wikis at the same time for a
265 // wiki farm.
266 $localIdentity = $this->userIdentityLookup->getUserIdentityByName(
267 $userName
268 );
269 } else {
270 // For local users, we can directly use $targetUser
271 $localIdentity = $targetUser;
272 }
273
274 if ( $localIdentity instanceof UserIdentity ) {
275 $isExpired = $this->tempUserDetailsLookup->isExpired(
276 $localIdentity
277 );
278 }
279 }
280
281 // Adjust the styling of expired temporary account links (T358469).
282 if ( $isExpired ) {
283 $classes[] = 'mw-tempuserlink-expired';
284
285 $description = $messageLocalizer->msg(
286 'tempuser-expired-link-tooltip'
287 )->text();
288
289 $postfix = Html::element(
290 'span',
291 [
292 'role' => 'presentation',
293 'class' => 'cdx-tooltip mw-tempuserlink-expired--tooltip',
294 ],
295 $description
296 );
297
298 $attributes['aria-description'] = $description;
299
300 // Hide default link title when rendering expired temporary account
301 // links to avoid conflicting with the tooltip.
302 $attributes['title'] = null;
303 }
304
305 return [
306 'classes' => $classes,
307 'extraAttr' => $attributes,
308 'postfix' => $postfix
309 ];
310 }
311
318 protected function isFromExternalWiki( $wikiId ): bool {
319 if ( $wikiId === WikiAwareEntity::LOCAL ) {
320 return false;
321 }
322
323 return !WikiMap::isCurrentWikiDbDomain( $wikiId );
324 }
325
340 private function getLinkClassesFromUserName( string $userName, bool $isDefaultCaption ): array {
341 $classes = [];
342
343 // mw-tempuserlink should be added only in cases like [[User:~2025-1|~2025-1]] and
344 // not e.g. [[User:~2025-1|this user]]: T398952#10994965.
345 if ( $isDefaultCaption && $this->tempUserConfig->isTempName( $userName ) ) {
346 $classes[] = 'mw-tempuserlink';
347 }
348 return $classes;
349 }
350
361 public function getLinkClasses( LinkTarget $target, bool $isDefaultCaption = false ): array {
362 $ns = $target->getNamespace();
363 $userName = null;
364 if ( $ns === NS_USER ) {
365 // Recognize direct links to users
366 $userName = $target->getText();
367 } elseif ( $ns === NS_SPECIAL ) {
368 // Recognize links to contributions pages
369 [ $name, $subpage ] = $this->specialPageFactory->resolveAlias( $target->getText() );
370 if ( $name === 'Contributions' && $subpage !== null ) {
371 $userName = $subpage;
372 }
373 }
374 if ( $userName !== null ) {
375 $userName = $this->userNameUtils->getCanonical( $userName );
376 if ( $userName !== false ) {
377 return $this->getLinkClassesFromUserName( $userName, $isDefaultCaption );
378 }
379 }
380 return [];
381 }
382}
const NS_USER
Definition Defines.php:53
const NS_SPECIAL
Definition Defines.php:40
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Class that generates HTML for internal links.
Service class that renders HTML for user-related links.
isFromExternalWiki( $wikiId)
Checks whether a given wiki identifier belongs to an external wiki.
__construct(HookContainer $hookContainer, TempUserConfig $tempUserConfig, SpecialPageFactory $specialPageFactory, LinkRenderer $linkRenderer, TempUserDetailsLookup $tempUserDetailsLookup, UserIdentityLookup $userIdentityLookup, UserNameUtils $userNameUtils)
getLinkClasses(LinkTarget $target, bool $isDefaultCaption=false)
Convenience function for LinkRenderer: return the CSS classes to add to a given LinkTarget if it repr...
userLink(UserIdentity $targetUser, IContextSource $context, ?string $altUserName=null, array $attributes=[])
Render a user page link (or user contributions for anonymous and temporary users).
Factory for handling the special page list and generating SpecialPage objects.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Class to parse and build external user names.
Caching lookup service for metadata related to temporary accounts, such as expiration.
UserNameUtils service.
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Store key-value entries in a size-limited in-memory LRU cache.
Interface for objects which can provide a MediaWiki context on request.
Marker interface for entities aware of the wiki they belong to.
getWikiId()
Get the ID of the wiki this page belongs to.
Interface for localizing messages in MediaWiki.
Represents the target of a wiki link.
getText()
Get the main part of the link target, in text form.
Interface for temporary user creation config and name matching.
Service for looking up UserIdentity.
Interface for objects representing user identity.
element(SerializerNode $parent, SerializerNode $node, $contents)
array $params
The job parameters.