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