Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.70% covered (danger)
8.70%
71 / 816
6.82% covered (danger)
6.82%
3 / 44
CRAP
0.00% covered (danger)
0.00%
0 / 1
Linker
8.70% covered (danger)
8.70%
71 / 816
6.82% covered (danger)
6.82%
3 / 44
69250.53
0.00% covered (danger)
0.00%
0 / 1
 link
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 linkKnown
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeSelfLinkObj
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getInvalidTitleDescription
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 fnamePart
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 makeExternalImage
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 makeImageLink
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 1
2862
 getImageLinkMTOParams
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
110
 makeThumbLinkObj
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 makeThumbLink2
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 1
2256
 processResponsiveImages
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
132
 makeBrokenImageLinkObj
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
132
 getUploadUrl
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 makeMediaLinkObj
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 makeMediaLinkFile
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 specialLink
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 makeExternalLink
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 userLink
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 userToolLinkArray
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
210
 renderUserToolLinksArray
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 userToolLinks
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 userToolLinksRedContribs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userTalkLink
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
4.94
 blockLink
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
2.86
 emailLink
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
2.86
 revUserLink
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getRevisionDeletedClass
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 revUserTools
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 expandLocalLinks
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 normalizeSubpageLink
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
210
 formatRevisionSize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 splitTrail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 generateRollback
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
5.06
 getRollbackEditCount
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
132
 buildRollbackLink
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
132
 formatHiddenCategories
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getContextFromMain
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 titleAttrib
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
132
 accesskey
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 getRevDeleteLink
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 revDeleteLink
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 revDeleteLinkDisabled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 tooltipAndAccesskeyAttribs
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 tooltip
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Methods to make links and related items.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Linker;
10
11use MediaTransformError;
12use MediaTransformOutput;
13use MediaWiki\Context\ContextSource;
14use MediaWiki\Context\DerivativeContext;
15use MediaWiki\Context\IContextSource;
16use MediaWiki\Context\RequestContext;
17use MediaWiki\FileRepo\File\File;
18use MediaWiki\HookContainer\HookRunner;
19use MediaWiki\Html\Html;
20use MediaWiki\Html\HtmlHelper;
21use MediaWiki\MainConfigNames;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Message\Message;
24use MediaWiki\Parser\Parser;
25use MediaWiki\Permissions\Authority;
26use MediaWiki\Revision\RevisionRecord;
27use MediaWiki\SpecialPage\SpecialPage;
28use MediaWiki\Title\Title;
29use MediaWiki\Title\TitleValue;
30use MediaWiki\User\ExternalUserNames;
31use MediaWiki\User\UserIdentityValue;
32use MessageLocalizer;
33use Wikimedia\Assert\Assert;
34use Wikimedia\HtmlArmor\HtmlArmor;
35use Wikimedia\Rdbms\SelectQueryBuilder;
36use Wikimedia\RemexHtml\Serializer\SerializerNode;
37
38/**
39 * Some internal bits split of from Skin.php. These functions are used
40 * for primarily page content: links, embedded images, table of contents. Links
41 * are also used in the skin.
42 *
43 * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
44 *
45 * @ingroup Skins
46 */
47class Linker {
48    /**
49     * Flags for userToolLinks()
50     */
51    public const TOOL_LINKS_NOBLOCK = 1;
52    public const TOOL_LINKS_EMAIL = 2;
53
54    /**
55     * This function returns an HTML link to the given target.  It serves a few
56     * purposes:
57     *   1) If $target is a LinkTarget, the correct URL to link to will be figured
58     *      out automatically.
59     *   2) It automatically adds the usual classes for various types of link
60     *      targets: "new" for red links, "stub" for short articles, etc.
61     *   3) It escapes all attribute values safely so there's no risk of XSS.
62     *   4) It provides a default tooltip if the target is a LinkTarget (the page
63     *      name of the target).
64     * link() replaces the old functions in the makeLink() family.
65     *
66     * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18.
67     * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead
68     *
69     * @param LinkTarget $target Can currently only be a LinkTarget, but this may
70     *   change to support Images, literal URLs, etc.
71     * @param string|null $html The HTML contents of the <a> element, i.e.,
72     *   the link text.  This is raw HTML and will not be escaped.  If null,
73     *   defaults to the prefixed text of the LinkTarget; or if the LinkTarget is just a
74     *   fragment, the contents of the fragment.
75     * @param array $customAttribs A key => value array of extra HTML attributes,
76     *   such as title and class.  (href is ignored.)  Classes will be
77     *   merged with the default classes, while other attributes will replace
78     *   default attributes.  All passed attribute values will be HTML-escaped.
79     *   A false attribute value means to suppress that attribute.
80     * @param array $query The query string to append to the URL
81     *   you're linking to, in key => value array form.  Query keys and values
82     *   will be URL-encoded.
83     * @param string|array $options String or array of strings:
84     *     'known': Page is known to exist, so don't check if it does.
85     *     'broken': Page is known not to exist, so don't check if it does.
86     *     'noclasses': Don't add any classes automatically (includes "new",
87     *       "stub", "mw-redirect", "extiw").  Only use the class attribute
88     *       provided, if any, so you get a simple blue link with no icons.
89     *     'forcearticlepath': Use the article path always, even with a querystring.
90     *       Has compatibility issues on some setups, so avoid wherever possible.
91     *     'http': Force a full URL with http:// as the scheme.
92     *     'https': Force a full URL with https:// as the scheme.
93     * @return string HTML <a> attribute
94     */
95    public static function link(
96        $target, $html = null, $customAttribs = [], $query = [], $options = []
97    ) {
98        if ( !$target instanceof LinkTarget ) {
99            wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
100            return "<!-- ERROR -->$html";
101        }
102
103        $services = MediaWikiServices::getInstance();
104        $options = (array)$options;
105        if ( $options ) {
106            // Custom options, create new LinkRenderer
107            $linkRenderer = $services->getLinkRendererFactory()
108                ->createFromLegacyOptions( $options );
109        } else {
110            $linkRenderer = $services->getLinkRenderer();
111        }
112
113        if ( $html !== null ) {
114            $text = new HtmlArmor( $html );
115        } else {
116            $text = null;
117        }
118
119        if ( in_array( 'known', $options, true ) ) {
120            return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
121        }
122
123        if ( in_array( 'broken', $options, true ) ) {
124            return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
125        }
126
127        if ( in_array( 'noclasses', $options, true ) ) {
128            return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query );
129        }
130
131        return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
132    }
133
134    /**
135     * Identical to link(), except $options defaults to 'known'.
136     *
137     * @since 1.16.3
138     * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead
139     * @see Linker::link
140     * @param LinkTarget $target
141     * @param-taint $target none
142     * @param string|null $html
143     * @param-taint $html exec_html
144     * @param array $customAttribs
145     * @param-taint $customAttribs none
146     * @param array $query
147     * @param-taint $query none
148     * @param string|array $options
149     * @param-taint $options none
150     * @return string
151     * @return-taint escaped
152     */
153    public static function linkKnown(
154        $target, $html = null, $customAttribs = [],
155        $query = [], $options = [ 'known' ]
156    ) {
157        return self::link( $target, $html, $customAttribs, $query, $options );
158    }
159
160    /**
161     * Make appropriate markup for a link to the current article. This is since
162     * MediaWiki 1.29.0 rendered as an <a> tag without an href and with a class
163     * showing the link text. The calling sequence is the same as for the other
164     * make*LinkObj static functions, but $query is not used.
165     *
166     * @since 1.16.3
167     * @param LinkTarget $nt
168     * @param string $html
169     * @param string $query
170     * @param string $trail
171     * @param string $prefix
172     * @param string $hash hash fragment since 1.40. Should be properly escaped using
173     *   Sanitizer::escapeIdForLink before being passed to this function.
174     *
175     * @return string
176     */
177    public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '', $hash = '' ) {
178        $nt = Title::newFromLinkTarget( $nt );
179        $attrs = [];
180        if ( $hash ) {
181            $attrs['class'] = 'mw-selflink-fragment';
182            $attrs['href'] = '#' . $hash;
183        } else {
184            // For backwards compatibility with gadgets we add selflink as well.
185            $attrs['class'] = 'mw-selflink selflink';
186        }
187        $ret = Html::rawElement( 'a', $attrs, $prefix . $html ) . $trail;
188        $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
189        if ( !$hookRunner->onSelfLinkBegin( $nt, $html, $trail, $prefix, $ret ) ) {
190            return $ret;
191        }
192
193        if ( $html == '' ) {
194            $html = htmlspecialchars( $nt->getPrefixedText() );
195        }
196        [ $inside, $trail ] = self::splitTrail( $trail );
197        return Html::rawElement( 'a', $attrs, $prefix . $html . $inside ) . $trail;
198    }
199
200    /**
201     * Get a message saying that an invalid title was encountered.
202     * This should be called after a method like Title::makeTitleSafe() returned
203     * a value indicating that the title object is invalid.
204     *
205     * @param IContextSource $context Context to use to get the messages
206     * @param int $namespace Namespace number
207     * @param string $title Text of the title, without the namespace part
208     * @return string
209     */
210    public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
211        // First we check whether the namespace exists or not.
212        if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
213            if ( $namespace == NS_MAIN ) {
214                $name = $context->msg( 'blanknamespace' )->text();
215            } else {
216                $name = MediaWikiServices::getInstance()->getContentLanguage()->
217                    getFormattedNsText( $namespace );
218            }
219            return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
220        }
221
222        return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
223    }
224
225    /**
226     * Returns the filename part of an url.
227     * Used as alternative text for external images.
228     *
229     * @param string $url
230     *
231     * @return string
232     */
233    private static function fnamePart( $url ) {
234        $basename = strrchr( $url, '/' );
235        if ( $basename === false ) {
236            $basename = $url;
237        } else {
238            $basename = substr( $basename, 1 );
239        }
240        return $basename;
241    }
242
243    /**
244     * Return the code for images which were added via external links,
245     * via Parser::maybeMakeExternalImage().
246     *
247     * @since 1.16.3
248     * @param string $url
249     * @param string $alt
250     *
251     * @return string
252     */
253    public static function makeExternalImage( $url, $alt = '' ) {
254        if ( $alt == '' ) {
255            $alt = self::fnamePart( $url );
256        }
257        $img = '';
258        $success = ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
259            ->onLinkerMakeExternalImage( $url, $alt, $img );
260        if ( !$success ) {
261            wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
262                . "with url {$url} and alt text {$alt} to {$img}" );
263            return $img;
264        }
265        return Html::element( 'img',
266            [
267                'src' => $url,
268                'alt' => $alt
269            ]
270        );
271    }
272
273    /**
274     * Given parameters derived from [[Image:Foo|options...]], generate the
275     * HTML that that syntax inserts in the page.
276     *
277     * @param Parser $parser
278     * @param LinkTarget $title LinkTarget object of the file (not the currently viewed page)
279     * @param File|false $file File object, or false if it doesn't exist
280     * @param array $frameParams Associative array of parameters external to the media handler.
281     *     Boolean parameters are indicated by presence or absence, the value is arbitrary and
282     *     will often be false.
283     *          thumbnail       If present, downscale and frame
284     *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
285     *          framed          Shows image in original size in a frame
286     *          frameless       Downscale but don't frame
287     *          upright         If present, tweak default sizes for portrait orientation
288     *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
289     *          border          If present, show a border around the image
290     *          align           Horizontal alignment (left, right, center, none)
291     *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle,
292     *                          bottom, text-bottom)
293     *          alt             Alternate text for image (i.e. alt attribute). Plain text.
294     *          title           Used for tooltips if caption isn't visible.
295     *          class           HTML for image classes. Plain text.
296     *          caption         HTML for image caption.
297     *          link-url        URL to link to
298     *          link-title      LinkTarget object to link to
299     *          link-target     Value for the target attribute, only with link-url
300     *          no-link         Boolean, suppress description link
301     *
302     * @param array $handlerParams Associative array of media handler parameters, to be passed
303     *       to transform(). Typical keys are "width" and "page".
304     *          targetlang      (optional) Target language code, see Parser::getTargetLanguage()
305     * @param string|false $time Timestamp of the file, set as false for current
306     * @param string $query Query params for desc url
307     * @param int|null $widthOption Used by the parser to remember the user preference thumbnailsize
308     * @since 1.20
309     * @return string HTML for an image, with links, wrappers, etc.
310     */
311    public static function makeImageLink( Parser $parser, LinkTarget $title,
312        $file, $frameParams = [], $handlerParams = [], $time = false,
313        $query = '', $widthOption = null
314    ) {
315        $title = Title::newFromLinkTarget( $title );
316        $res = null;
317        $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
318        if ( !$hookRunner->onImageBeforeProduceHTML( null, $title,
319            // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
320            $file, $frameParams, $handlerParams, $time, $res,
321            // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
322            $parser, $query, $widthOption )
323        ) {
324            return $res;
325        }
326
327        if ( $file && !$file->allowInlineDisplay() ) {
328            wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ' does not allow inline display' );
329            return self::link( $title );
330        }
331
332        // Clean up parameters
333        $page = $handlerParams['page'] ?? false;
334        if ( !isset( $frameParams['align'] ) ) {
335            $frameParams['align'] = '';
336        }
337        if ( !isset( $frameParams['title'] ) ) {
338            $frameParams['title'] = '';
339        }
340        if ( !isset( $frameParams['class'] ) ) {
341            $frameParams['class'] = '';
342        }
343
344        $services = MediaWikiServices::getInstance();
345        $config = $services->getMainConfig();
346
347        $classes = [];
348        if (
349            !isset( $handlerParams['width'] ) &&
350            !isset( $frameParams['manualthumb'] ) &&
351            !isset( $frameParams['framed'] )
352        ) {
353            $classes[] = 'mw-default-size';
354        }
355
356        $prefix = $postfix = '';
357
358        if ( $file && !isset( $handlerParams['width'] ) ) {
359            if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
360                // If its a vector image, and user only specifies height
361                // we don't want it to be limited by its "normal" width.
362                $svgMaxSize = $config->get( MainConfigNames::SVGMaxSize );
363                $handlerParams['width'] = $svgMaxSize;
364            } else {
365                $handlerParams['width'] = $file->getWidth( $page );
366            }
367
368            if ( isset( $frameParams['thumbnail'] )
369                || isset( $frameParams['manualthumb'] )
370                || isset( $frameParams['framed'] )
371                || isset( $frameParams['frameless'] )
372                || !$handlerParams['width']
373            ) {
374                $thumbLimits = $config->get( MainConfigNames::ThumbLimits );
375                $thumbUpright = $config->get( MainConfigNames::ThumbUpright );
376                if ( $widthOption === null || !isset( $thumbLimits[$widthOption] ) ) {
377                    $userOptionsLookup = $services->getUserOptionsLookup();
378                    $widthOption = $userOptionsLookup->getDefaultOption( 'thumbsize' );
379                }
380
381                // Reduce width for upright images when parameter 'upright' is used
382                if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
383                    $frameParams['upright'] = $thumbUpright;
384                }
385
386                // For caching health: If width scaled down due to upright
387                // parameter, round to full __0 pixel to avoid the creation of a
388                // lot of odd thumbs.
389                $prefWidth = isset( $frameParams['upright'] ) ?
390                    round( $thumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
391                    $thumbLimits[$widthOption];
392
393                // Use width which is smaller: real image width or user preference width
394                // Unless image is scalable vector.
395                if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
396                        $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
397                    $handlerParams['width'] = $prefWidth;
398                }
399            }
400        }
401
402        // Parser::makeImage has a similarly named variable
403        $hasVisibleCaption = isset( $frameParams['thumbnail'] ) ||
404            isset( $frameParams['manualthumb'] ) ||
405            isset( $frameParams['framed'] );
406
407        if ( $hasVisibleCaption ) {
408            return $prefix . self::makeThumbLink2(
409                $title, $file, $frameParams, $handlerParams, $time, $query,
410                $classes, $parser
411            ) . $postfix;
412        }
413
414        $rdfaType = 'mw:File';
415
416        if ( isset( $frameParams['frameless'] ) ) {
417            $rdfaType .= '/Frameless';
418            if ( $file ) {
419                $srcWidth = $file->getWidth( $page );
420                # For "frameless" option: do not present an image bigger than the
421                # source (for bitmap-style images). This is the same behavior as the
422                # "thumb" option does it already.
423                if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
424                    $handlerParams['width'] = $srcWidth;
425                }
426            }
427        }
428
429        if ( $file && isset( $handlerParams['width'] ) ) {
430            # Create a resized image, without the additional thumbnail features
431            $thumb = $file->transform( $handlerParams );
432        } else {
433            $thumb = false;
434        }
435
436        $isBadFile = $file && $thumb &&
437            $parser->getBadFileLookup()->isBadFile( $title->getDBkey(), $parser->getTitle() );
438
439        if ( !$thumb || $thumb->isError() || $isBadFile ) {
440            $rdfaType = 'mw:Error ' . $rdfaType;
441            $currentExists = $file && $file->exists();
442            if ( $currentExists && !$thumb ) {
443                $label = wfMessage( 'thumbnail_error', '' )->text();
444            } elseif ( $thumb && $thumb->isError() ) {
445                Assert::invariant(
446                    $thumb instanceof MediaTransformError,
447                    'Unknown MediaTransformOutput: ' . get_class( $thumb )
448                );
449                $label = $thumb->toText();
450            } else {
451                $label = $frameParams['alt'] ?? '';
452            }
453            $s = self::makeBrokenImageLinkObj(
454                $title, $label, '', '', '', (bool)$time, $handlerParams, $currentExists
455            );
456        } else {
457            self::processResponsiveImages( $file, $thumb, $handlerParams );
458            $params = [];
459            // An empty alt indicates an image is not a key part of the content
460            // and that non-visual browsers may omit it from rendering.  Only
461            // set the parameter if it's explicitly requested.
462            if ( isset( $frameParams['alt'] ) ) {
463                $params['alt'] = $frameParams['alt'];
464            }
465            $params['title'] = $frameParams['title'];
466            $params += [
467                'img-class' => 'mw-file-element',
468            ];
469            $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
470            $s = $thumb->toHtml( $params );
471        }
472
473        $wrapper = 'span';
474        $caption = '';
475
476        if ( $frameParams['align'] != '' ) {
477            $wrapper = 'figure';
478            // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
479            $classes[] = "mw-halign-{$frameParams['align']}";
480            $caption = Html::rawElement(
481                'figcaption', [], $frameParams['caption'] ?? ''
482            );
483        } elseif ( isset( $frameParams['valign'] ) ) {
484            // Possible values: mw-valign-middle mw-valign-baseline mw-valign-sub
485            // mw-valign-super mw-valign-top mw-valign-text-top mw-valign-bottom
486            // mw-valign-text-bottom
487            $classes[] = "mw-valign-{$frameParams['valign']}";
488        }
489
490        if ( isset( $frameParams['border'] ) ) {
491            $classes[] = 'mw-image-border';
492        }
493
494        if ( isset( $frameParams['class'] ) ) {
495            $classes[] = $frameParams['class'];
496        }
497
498        $attribs = [
499            'class' => $classes,
500            'typeof' => $rdfaType,
501        ];
502
503        $s = Html::rawElement( $wrapper, $attribs, $s . $caption );
504
505        return str_replace( "\n", ' ', $s );
506    }
507
508    /**
509     * Get the link parameters for MediaTransformOutput::toHtml() from given
510     * frame parameters supplied by the Parser.
511     * @param array $frameParams The frame parameters
512     * @param string $query An optional query string to add to description page links
513     * @param Parser|null $parser
514     * @return array
515     */
516    public static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
517        $mtoParams = [];
518        if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
519            $mtoParams['custom-url-link'] = $frameParams['link-url'];
520            if ( isset( $frameParams['link-target'] ) ) {
521                $mtoParams['custom-target-link'] = $frameParams['link-target'];
522            }
523            if ( $parser ) {
524                $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
525                foreach ( $extLinkAttrs as $name => $val ) {
526                    // Currently could include 'rel' and 'target'
527                    $mtoParams['parser-extlink-' . $name] = $val;
528                }
529            }
530        } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
531            $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
532            $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
533                $linkRenderer->normalizeTarget( $frameParams['link-title'] )
534            );
535            if ( isset( $frameParams['link-title-query'] ) ) {
536                $mtoParams['custom-title-link-query'] = $frameParams['link-title-query'];
537            }
538        } elseif ( !empty( $frameParams['no-link'] ) ) {
539            // No link
540        } else {
541            $mtoParams['desc-link'] = true;
542            $mtoParams['desc-query'] = $query;
543        }
544        return $mtoParams;
545    }
546
547    /**
548     * Make HTML for a thumbnail including image, border and caption
549     * @param LinkTarget $title
550     * @param File|false $file File object or false if it doesn't exist
551     * @param string $label
552     * @param string $alt
553     * @param string|null $align
554     * @param array $params
555     * @param bool $framed
556     * @param string $manualthumb
557     * @return string
558     */
559    public static function makeThumbLinkObj(
560        LinkTarget $title, $file, $label = '', $alt = '', $align = null,
561        $params = [], $framed = false, $manualthumb = ''
562    ) {
563        $frameParams = [
564            'alt' => $alt,
565            'caption' => $label,
566            'align' => $align
567        ];
568        $classes = [];
569        if ( $manualthumb ) {
570            $frameParams['manualthumb'] = $manualthumb;
571        } elseif ( $framed ) {
572            $frameParams['framed'] = true;
573        } elseif ( !isset( $params['width'] ) ) {
574            $classes[] = 'mw-default-size';
575        }
576        return self::makeThumbLink2(
577            $title, $file, $frameParams, $params, false, '', $classes
578        );
579    }
580
581    /**
582     * @param LinkTarget $title
583     * @param File|false $file
584     * @param array $frameParams
585     * @param array $handlerParams
586     * @param bool $time If a file of a certain timestamp was requested
587     * @param string $query
588     * @param string[] $classes @since 1.36
589     * @param Parser|null $parser @since 1.38
590     * @return string
591     */
592    public static function makeThumbLink2(
593        LinkTarget $title, $file, $frameParams = [], $handlerParams = [],
594        $time = false, $query = '', array $classes = [], ?Parser $parser = null
595    ) {
596        $exists = $file && $file->exists();
597        $services = MediaWikiServices::getInstance();
598
599        $page = $handlerParams['page'] ?? false;
600        $lang = $handlerParams['lang'] ?? false;
601
602        if ( !isset( $frameParams['align'] ) ) {
603            $frameParams['align'] = '';
604        }
605        if ( !isset( $frameParams['caption'] ) ) {
606            $frameParams['caption'] = '';
607        }
608
609        if ( empty( $handlerParams['width'] ) ) {
610            // Reduce width for upright images when parameter 'upright' is used
611            $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
612        }
613
614        $thumb = false;
615        $noscale = false;
616        $manualthumb = false;
617        $manual_title = '';
618        $rdfaType = 'mw:File/Thumb';
619
620        if ( !$exists ) {
621            // Same precedence as the $exists case
622            if ( !isset( $frameParams['manualthumb'] ) && isset( $frameParams['framed'] ) ) {
623                $rdfaType = 'mw:File/Frame';
624            }
625            $outerWidth = $handlerParams['width'] + 2;
626        } else {
627            if ( isset( $frameParams['manualthumb'] ) ) {
628                # Use manually specified thumbnail
629                $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
630                if ( $manual_title ) {
631                    $manual_img = $services->getRepoGroup()
632                        ->findFile( $manual_title );
633                    if ( $manual_img ) {
634                        $thumb = $manual_img->getUnscaledThumb( $handlerParams );
635                        $manualthumb = true;
636                    }
637                }
638            } else {
639                $srcWidth = $file->getWidth( $page );
640                if ( isset( $frameParams['framed'] ) ) {
641                    $rdfaType = 'mw:File/Frame';
642                    if ( !$file->isVectorized() ) {
643                        // Use image dimensions, don't scale
644                        $noscale = true;
645                    } else {
646                        // framed is unscaled, but for vectorized images
647                        // we need to a width for scaling up for the high density variants
648                        $handlerParams['width'] = $srcWidth;
649                    }
650                }
651
652                // Do not present an image bigger than the source, for bitmap-style images
653                // This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
654                if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
655                    $handlerParams['width'] = $srcWidth;
656                }
657
658                $thumb = $noscale
659                    ? $file->getUnscaledThumb( $handlerParams )
660                    : $file->transform( $handlerParams );
661            }
662
663            if ( $thumb ) {
664                $outerWidth = $thumb->getWidth() + 2;
665            } else {
666                $outerWidth = $handlerParams['width'] + 2;
667            }
668        }
669
670        if ( $parser && $rdfaType === 'mw:File/Thumb' ) {
671            $parser->getOutput()->addModules( [ 'mediawiki.page.media' ] );
672        }
673
674        $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
675        $linkTitleQuery = [];
676        if ( $page || $lang ) {
677            if ( $page ) {
678                $linkTitleQuery['page'] = $page;
679            }
680            if ( $lang ) {
681                $linkTitleQuery['lang'] = $lang;
682            }
683            # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
684            # So we don't need to pass it here in $query. However, the URL for the
685            # zoom icon still needs it, so we make a unique query for it. See T16771
686            $url = wfAppendQuery( $url, $linkTitleQuery );
687        }
688
689        if ( $manualthumb
690            && !isset( $frameParams['link-title'] )
691            && !isset( $frameParams['link-url'] )
692            && !isset( $frameParams['no-link'] ) ) {
693            $frameParams['link-title'] = $title;
694            $frameParams['link-title-query'] = $linkTitleQuery;
695        }
696
697        if ( $frameParams['align'] != '' ) {
698            // Possible values: mw-halign-left mw-halign-center mw-halign-right mw-halign-none
699            $classes[] = "mw-halign-{$frameParams['align']}";
700        }
701
702        if ( isset( $frameParams['class'] ) ) {
703            $classes[] = $frameParams['class'];
704        }
705
706        $s = '';
707
708        $isBadFile = $exists && $thumb && $parser &&
709            $parser->getBadFileLookup()->isBadFile(
710                $manualthumb ? $manual_title->getDBkey() : $title->getDBkey(),
711                $parser->getTitle()
712            );
713
714        if ( !$exists ) {
715            $rdfaType = 'mw:Error ' . $rdfaType;
716            $label = $frameParams['alt'] ?? '';
717            $s .= self::makeBrokenImageLinkObj(
718                $title, $label, '', '', '', (bool)$time, $handlerParams, false
719            );
720            $zoomIcon = '';
721        } elseif ( !$thumb || $thumb->isError() || $isBadFile ) {
722            $rdfaType = 'mw:Error ' . $rdfaType;
723            if ( $thumb && $thumb->isError() ) {
724                Assert::invariant(
725                    $thumb instanceof MediaTransformError,
726                    'Unknown MediaTransformOutput: ' . get_class( $thumb )
727                );
728                $label = $thumb->toText();
729            } elseif ( !$thumb ) {
730                $label = wfMessage( 'thumbnail_error', '' )->text();
731            } else {
732                $label = '';
733            }
734            $s .= self::makeBrokenImageLinkObj(
735                $title, $label, '', '', '', (bool)$time, $handlerParams, true
736            );
737            $zoomIcon = '';
738        } else {
739            if ( !$noscale && !$manualthumb ) {
740                self::processResponsiveImages( $file, $thumb, $handlerParams );
741            }
742            $params = [];
743            // An empty alt indicates an image is not a key part of the content
744            // and that non-visual browsers may omit it from rendering.  Only
745            // set the parameter if it's explicitly requested.
746            if ( isset( $frameParams['alt'] ) ) {
747                $params['alt'] = $frameParams['alt'];
748            }
749            $params += [
750                'img-class' => 'mw-file-element',
751            ];
752            // Only thumbs gets the magnify link
753            if ( $rdfaType === 'mw:File/Thumb' ) {
754                $params['magnify-resource'] = $url;
755            }
756            $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
757            $s .= $thumb->toHtml( $params );
758            if ( isset( $frameParams['framed'] ) ) {
759                $zoomIcon = '';
760            } else {
761                $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
762                    Html::rawElement( 'a', [
763                        'href' => $url,
764                        'class' => 'internal',
765                        'title' => wfMessage( 'thumbnail-more' )->text(),
766                    ] )
767                );
768            }
769        }
770
771        $s .= Html::rawElement(
772            'figcaption', [], $frameParams['caption'] ?? ''
773        );
774
775        $attribs = [
776            'class' => $classes,
777            'typeof' => $rdfaType,
778        ];
779
780        $s = Html::rawElement( 'figure', $attribs, $s );
781
782        return str_replace( "\n", ' ', $s );
783    }
784
785    /**
786     * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
787     * applicable.
788     *
789     * @param File $file
790     * @param MediaTransformOutput|null $thumb
791     * @param array $hp Image parameters
792     */
793    public static function processResponsiveImages( $file, $thumb, $hp ) {
794        $responsiveImages = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ResponsiveImages );
795        if ( $responsiveImages && $thumb && !$thumb->isError() ) {
796            $hp15 = $hp;
797            $hp15['width'] = round( $hp['width'] * 1.5 );
798            $hp20 = $hp;
799            $hp20['width'] = $hp['width'] * 2;
800            if ( isset( $hp['height'] ) ) {
801                $hp15['height'] = round( $hp['height'] * 1.5 );
802                $hp20['height'] = $hp['height'] * 2;
803            }
804
805            $thumb15 = $file->transform( $hp15 );
806            $thumb20 = $file->transform( $hp20 );
807            if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
808                $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
809            }
810            if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
811                $thumb->responsiveUrls['2'] = $thumb20->getUrl();
812            }
813        }
814    }
815
816    /**
817     * Make a "broken" link to an image
818     *
819     * @since 1.16.3
820     * @param LinkTarget $title
821     * @param string $label Link label (plain text)
822     * @param string $query Query string
823     * @param string $unused1 Unused parameter kept for b/c
824     * @param string $unused2 Unused parameter kept for b/c
825     * @param bool $time A file of a certain timestamp was requested
826     * @param array $handlerParams @since 1.36
827     * @param bool $currentExists @since 1.41
828     * @return string
829     */
830    public static function makeBrokenImageLinkObj(
831        $title, $label = '', $query = '', $unused1 = '', $unused2 = '',
832        $time = false, array $handlerParams = [], bool $currentExists = false
833    ) {
834        if ( !$title instanceof LinkTarget ) {
835            wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
836            return "<!-- ERROR -->" . htmlspecialchars( $label );
837        }
838
839        $title = Title::newFromLinkTarget( $title );
840        $services = MediaWikiServices::getInstance();
841        $mainConfig = $services->getMainConfig();
842        $enableUploads = $mainConfig->get( MainConfigNames::EnableUploads );
843        $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
844        $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
845        if ( $label == '' ) {
846            $label = $title->getPrefixedText();
847        }
848
849        $html = Html::element( 'span', [
850            'class' => 'mw-file-element mw-broken-media',
851            // These data attributes are used to dynamically size the span, see T273013
852            'data-width' => $handlerParams['width'] ?? null,
853            'data-height' => $handlerParams['height'] ?? null,
854        ], $label );
855
856        $repoGroup = $services->getRepoGroup();
857        $currentExists = $currentExists ||
858            ( $time && $repoGroup->findFile( $title ) !== false );
859
860        if ( ( $uploadMissingFileUrl || $uploadNavigationUrl || $enableUploads )
861            && !$currentExists
862        ) {
863            if (
864                $title->inNamespace( NS_FILE ) &&
865                $repoGroup->getLocalRepo()->checkRedirect( $title )
866            ) {
867                // We already know it's a redirect, so mark it accordingly
868                return self::link(
869                    $title,
870                    $html,
871                    [ 'class' => 'mw-redirect' ],
872                    wfCgiToArray( $query ),
873                    [ 'known', 'noclasses' ]
874                );
875            }
876            return Html::rawElement( 'a', [
877                    'href' => self::getUploadUrl( $title, $query ),
878                    'class' => 'new',
879                    'title' => $title->getPrefixedText()
880                ], $html );
881        }
882        return self::link(
883            $title,
884            $html,
885            [],
886            wfCgiToArray( $query ),
887            [ 'known', 'noclasses' ]
888        );
889    }
890
891    /**
892     * Get the URL to upload a certain file
893     *
894     * @since 1.16.3
895     * @param LinkTarget $destFile LinkTarget object of the file to upload
896     * @param string $query Urlencoded query string to prepend
897     * @return string Urlencoded URL
898     */
899    public static function getUploadUrl( $destFile, $query = '' ) {
900        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
901        $uploadMissingFileUrl = $mainConfig->get( MainConfigNames::UploadMissingFileUrl );
902        $uploadNavigationUrl = $mainConfig->get( MainConfigNames::UploadNavigationUrl );
903        $q = 'wpDestFile=' . Title::newFromLinkTarget( $destFile )->getPartialURL();
904        if ( $query != '' ) {
905            $q .= '&' . $query;
906        }
907
908        if ( $uploadMissingFileUrl ) {
909            return wfAppendQuery( $uploadMissingFileUrl, $q );
910        }
911
912        if ( $uploadNavigationUrl ) {
913            return wfAppendQuery( $uploadNavigationUrl, $q );
914        }
915
916        $upload = SpecialPage::getTitleFor( 'Upload' );
917
918        return $upload->getLocalURL( $q );
919    }
920
921    /**
922     * Create a direct link to a given uploaded file.
923     *
924     * @since 1.16.3
925     * @param LinkTarget $title
926     * @param string $html Pre-sanitized HTML
927     * @param string|false $time MW timestamp of file creation time
928     * @return string HTML
929     */
930    public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
931        $img = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
932            $title, [ 'time' => $time ]
933        );
934        return self::makeMediaLinkFile( $title, $img, $html );
935    }
936
937    /**
938     * Create a direct link to a given uploaded file.
939     * This will make a broken link if $file is false.
940     *
941     * @since 1.16.3
942     * @param LinkTarget $title
943     * @param File|false $file File object or false
944     * @param string $html Pre-sanitized HTML
945     * @return string HTML
946     *
947     * @todo Handle invalid or missing images better.
948     */
949    public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
950        if ( $file && $file->exists() ) {
951            $url = $file->getUrl();
952            $class = 'internal';
953        } else {
954            $url = self::getUploadUrl( $title );
955            $class = 'new';
956        }
957
958        $alt = $title->getText();
959        if ( $html == '' ) {
960            $html = $alt;
961        }
962
963        $ret = '';
964        $attribs = [
965            'href' => $url,
966            'class' => $class,
967            'title' => $alt
968        ];
969
970        if ( !( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onLinkerMakeMediaLinkFile(
971            Title::newFromLinkTarget( $title ), $file, $html, $attribs, $ret )
972        ) {
973            wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
974                . "with url {$url} and text {$html} to {$ret}" );
975            return $ret;
976        }
977
978        return Html::rawElement( 'a', $attribs, $html );
979    }
980
981    /**
982     * Make a link to a special page given its name and, optionally,
983     * a message key from the link text.
984     * Usage example: Linker::specialLink( 'Recentchanges' )
985     *
986     * @since 1.16.3
987     * @param string $name Special page name, can optionally include â€¦/subpages and â€¦?parameters
988     * @param string $key Optional message key if different from $name
989     * @return string
990     */
991    public static function specialLink( $name, $key = '' ) {
992        $queryPos = strpos( $name, '?' );
993        if ( $queryPos !== false ) {
994            $getParams = wfCgiToArray( substr( $name, $queryPos + 1 ) );
995            $name = substr( $name, 0, $queryPos );
996        } else {
997            $getParams = [];
998        }
999
1000        $slashPos = strpos( $name, '/' );
1001        if ( $slashPos !== false ) {
1002            $subpage = substr( $name, $slashPos + 1 );
1003            $name = substr( $name, 0, $slashPos );
1004        } else {
1005            $subpage = false;
1006        }
1007
1008        if ( $key == '' ) {
1009            $key = strtolower( $name );
1010        }
1011
1012        return self::linkKnown(
1013            SpecialPage::getTitleFor( $name, $subpage ),
1014            wfMessage( $key )->escaped(),
1015            [],
1016            $getParams
1017        );
1018    }
1019
1020    /**
1021     * Make an external link
1022     *
1023     * @since 1.16.3. $title added in 1.21
1024     * @param string $url URL to link to
1025     * @param-taint $url escapes_html
1026     * @param string $text Text of link
1027     * @param-taint $text none
1028     * @param bool $escape Do we escape the link text?
1029     * @param-taint $escape none
1030     * @param string $linktype Type of external link. Gets added to the classes
1031     * @param-taint $linktype escapes_html
1032     * @param array $attribs Array of extra attributes to <a>
1033     * @param-taint $attribs escapes_html
1034     * @param LinkTarget|null $title LinkTarget object used for title specific link attributes
1035     * @param-taint $title none
1036     * @return string
1037     * @deprecated since 1.43; use LinkRenderer::makeExternalLink(), passing
1038     *   in an HtmlArmor instance if $escape was false.
1039     */
1040    public static function makeExternalLink( $url, $text, $escape = true,
1041        $linktype = '', $attribs = [], $title = null
1042    ) {
1043        // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
1044        global $wgTitle;
1045        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1046        return $linkRenderer->makeExternalLink(
1047            $url,
1048            $escape ? $text : new HtmlArmor( $text ),
1049            $title ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ),
1050            $linktype,
1051            $attribs
1052        );
1053    }
1054
1055    /**
1056     * Make user link (or user contributions for unregistered users)
1057     *
1058     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
1059     *
1060     * @param int $userId User id in database.
1061     * @param string $userName User name in database.
1062     * @param string|false $altUserName Text to display instead of the user name (optional)
1063     * @param string[] $attributes Extra HTML attributes. See Linker::link.
1064     * @return string HTML fragment
1065     * @since 1.16.3. $altUserName was added in 1.19. $attributes was added in 1.40.
1066     *
1067     * @deprecated since 1.44, use {@link LinkRenderer::makeUserLink()} instead.
1068     */
1069    public static function userLink(
1070        $userId,
1071        $userName,
1072        $altUserName = false,
1073        $attributes = []
1074    ) {
1075        if ( $userName === '' || $userName === false || $userName === null ) {
1076            wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1077                'that need to be fixed?' );
1078            return wfMessage( 'empty-username' )->parse();
1079        }
1080
1081        return MediaWikiServices::getInstance()->getLinkRenderer()
1082            ->makeUserLink(
1083                new UserIdentityValue( $userId, (string)$userName ),
1084                RequestContext::getMain(),
1085                $altUserName === false ? null : (string)$altUserName,
1086                $attributes
1087            );
1088    }
1089
1090    /**
1091     * Generate standard user tool links (talk, contributions, block link, etc.)
1092     *
1093     * @since 1.42
1094     * @param int $userId User identifier
1095     * @param string $userText User name or IP address
1096     * @param bool $redContribsWhenNoEdits Should the contributions link be
1097     *   red if the user has no edits?
1098     * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
1099     *   and Linker::TOOL_LINKS_EMAIL).
1100     * @param int|null $edits User edit count. If you enable $redContribsWhenNoEdits,
1101     *  you may pass a pre-computed edit count here, or 0 if the caller knows that
1102     *  the account has 0 edits. Otherwise, the value is unused and null may
1103     *  be passed. If $redContribsWhenNoEdits is enabled and null is passed, the
1104     *  edit count will be lazily fetched from UserEditTracker.
1105     * @return string[] Array of HTML fragments, each of them a link tag with a distinctive
1106     *   class; or a single string on error.
1107     */
1108    public static function userToolLinkArray(
1109        $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1110    ): array {
1111        $services = MediaWikiServices::getInstance();
1112        $disableAnonTalk = $services->getMainConfig()->get( MainConfigNames::DisableAnonTalk );
1113        $talkable = !( $disableAnonTalk && $userId == 0 );
1114        $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1115        $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1116
1117        if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
1118            // No tools for an external user
1119            return [];
1120        }
1121
1122        $items = [];
1123        if ( $talkable ) {
1124            $items[] = self::userTalkLink( $userId, $userText );
1125        }
1126        if ( $userId ) {
1127            // check if the user has an edit
1128            $attribs = [];
1129            $attribs['class'] = 'mw-usertoollinks-contribs';
1130            if ( $redContribsWhenNoEdits ) {
1131                if ( $edits === null ) {
1132                    $user = UserIdentityValue::newRegistered( $userId, $userText );
1133                    $edits = $services->getUserEditTracker()->getUserEditCount( $user );
1134                }
1135                if ( $edits === 0 ) {
1136                    // Note: "new" class is inappropriate here, as "new" class
1137                    // should only be used for pages that do not exist.
1138                    $attribs['class'] .= ' mw-usertoollinks-contribs-no-edits';
1139                }
1140            }
1141            $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1142
1143            $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1144        }
1145        $userCanBlock = RequestContext::getMain()->getAuthority()->isAllowed( 'block' );
1146        if ( $blockable && $userCanBlock ) {
1147            $items[] = self::blockLink( $userId, $userText );
1148        }
1149
1150        if (
1151            $addEmailLink
1152            && MediaWikiServices::getInstance()->getEmailUserFactory()
1153                ->newEmailUser( RequestContext::getMain()->getAuthority() )
1154                ->canSend()
1155                ->isGood()
1156        ) {
1157            $items[] = self::emailLink( $userId, $userText );
1158        }
1159
1160        ( new HookRunner( $services->getHookContainer() ) )->onUserToolLinksEdit( $userId, $userText, $items );
1161
1162        return $items;
1163    }
1164
1165    /**
1166     * Generate standard tool links HTML from a link array returned by userToolLinkArray().
1167     * @since 1.42
1168     * @param array $items
1169     * @param bool $useParentheses (optional, default true) Wrap comments in parentheses where needed
1170     * @return string
1171     */
1172    public static function renderUserToolLinksArray( array $items, bool $useParentheses ): string {
1173        global $wgLang;
1174
1175        if ( !$items ) {
1176            return '';
1177        }
1178
1179        if ( $useParentheses ) {
1180            return wfMessage( 'word-separator' )->escaped()
1181                . '<span class="mw-usertoollinks">'
1182                . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1183                . '</span>';
1184        }
1185
1186        $tools = [];
1187        foreach ( $items as $tool ) {
1188            $tools[] = Html::rawElement( 'span', [], $tool );
1189        }
1190        return ' <span class="mw-usertoollinks mw-changeslist-links">' .
1191            implode( ' ', $tools ) . '</span>';
1192    }
1193
1194    /**
1195     * Generate standard user tool links (talk, contributions, block link, etc.)
1196     *
1197     * @since 1.16.3
1198     * @param int $userId User identifier
1199     * @param string $userText User name or IP address
1200     * @param bool $redContribsWhenNoEdits Should the contributions link be
1201     *   red if the user has no edits?
1202     * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
1203     *   and Linker::TOOL_LINKS_EMAIL).
1204     * @param int|null $edits User edit count (optional, for performance)
1205     * @param bool $useParentheses (optional, default true) Wrap comments in parentheses where needed
1206     * @return string HTML fragment
1207     */
1208    public static function userToolLinks(
1209        $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
1210        $useParentheses = true
1211    ) {
1212        if ( $userText === '' ) {
1213            wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1214                'that need to be fixed?' );
1215            return ' ' . wfMessage( 'empty-username' )->parse();
1216        }
1217
1218        $items = self::userToolLinkArray( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
1219        return self::renderUserToolLinksArray( $items, $useParentheses );
1220    }
1221
1222    /**
1223     * Alias for userToolLinks( $userId, $userText, true );
1224     * @since 1.16.3
1225     * @param int $userId User identifier
1226     * @param string $userText User name or IP address
1227     * @param int|null $edits User edit count (optional, for performance)
1228     * @param bool $useParentheses (optional) Wrap comments in parentheses where needed
1229     * @return string
1230     */
1231    public static function userToolLinksRedContribs(
1232        $userId, $userText, $edits = null, $useParentheses = true
1233    ) {
1234        return self::userToolLinks( $userId, $userText, true, 0, $edits, $useParentheses );
1235    }
1236
1237    /**
1238     * @since 1.16.3
1239     * @param int $userId User id in database.
1240     * @param string $userText User name in database.
1241     * @return string HTML fragment with user talk link
1242     */
1243    public static function userTalkLink( $userId, $userText ) {
1244        if ( $userText === '' ) {
1245            wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1246                'that need to be fixed?' );
1247            return wfMessage( 'empty-username' )->parse();
1248        }
1249
1250        $userTalkPage = TitleValue::tryNew( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
1251        $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-talk' ];
1252        $linkText = wfMessage( 'talkpagelinktext' )->escaped();
1253
1254        return $userTalkPage
1255            ? self::link( $userTalkPage, $linkText, $moreLinkAttribs )
1256            : Html::rawElement( 'span', $moreLinkAttribs, $linkText );
1257    }
1258
1259    /**
1260     * @since 1.16.3
1261     * @param int $userId
1262     * @param string $userText User name in database.
1263     * @return string HTML fragment with block link
1264     */
1265    public static function blockLink( $userId, $userText ) {
1266        if ( $userText === '' ) {
1267            wfDebug( __METHOD__ . ' received an empty username. Are there database errors ' .
1268                'that need to be fixed?' );
1269            return wfMessage( 'empty-username' )->parse();
1270        }
1271
1272        $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1273        $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-block' ];
1274
1275        return self::link( $blockPage,
1276            wfMessage( 'blocklink' )->escaped(),
1277            $moreLinkAttribs
1278        );
1279    }
1280
1281    /**
1282     * @param int $userId
1283     * @param string $userText User name in database.
1284     * @return string HTML fragment with e-mail user link
1285     */
1286    public static function emailLink( $userId, $userText ) {
1287        if ( $userText === '' ) {
1288            wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
1289                'that need to be fixed?' );
1290            return wfMessage( 'empty-username' )->parse();
1291        }
1292
1293        $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1294        $moreLinkAttribs = [ 'class' => 'mw-usertoollinks-mail' ];
1295        return self::link( $emailPage,
1296            wfMessage( 'emaillink' )->escaped(),
1297            $moreLinkAttribs
1298        );
1299    }
1300
1301    /**
1302     * Generate a user link if the current user is allowed to view it
1303     *
1304     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
1305     *
1306     * @since 1.16.3
1307     * @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
1308     *    since 1.35)
1309     * @param bool $isPublic Show only if all users can see it
1310     * @return string HTML fragment
1311     */
1312    public static function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
1313        // TODO inject authority
1314        $authority = RequestContext::getMain()->getAuthority();
1315
1316        $revUser = $revRecord->getUser(
1317            $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1318            $authority
1319        );
1320        if ( $revUser ) {
1321            $link = self::userLink( $revUser->getId(), $revUser->getName() );
1322        } else {
1323            // User is deleted and we can't (or don't want to) view it
1324            $link = wfMessage( 'rev-deleted-user' )->escaped();
1325        }
1326
1327        if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1328            $class = self::getRevisionDeletedClass( $revRecord );
1329            return '<span class="' . $class . '">' . $link . '</span>';
1330        }
1331        return $link;
1332    }
1333
1334    /**
1335     * Returns css class of a deleted revision
1336     * @param RevisionRecord $revisionRecord
1337     * @return string 'history-deleted', 'mw-history-suppressed' added if suppressed too
1338     * @since 1.37
1339     */
1340    public static function getRevisionDeletedClass( RevisionRecord $revisionRecord ): string {
1341        $class = 'history-deleted';
1342        if ( $revisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
1343            $class .= ' mw-history-suppressed';
1344        }
1345        return $class;
1346    }
1347
1348    /**
1349     * Generate a user tool link cluster if the current user is allowed to view it
1350     *
1351     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
1352     *
1353     * @since 1.16.3
1354     * @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
1355     *    since 1.35)
1356     * @param bool $isPublic Show only if all users can see it
1357     * @param bool $useParentheses (optional) Wrap comments in parentheses where needed
1358     * @return string HTML
1359     */
1360    public static function revUserTools(
1361        RevisionRecord $revRecord,
1362        $isPublic = false,
1363        $useParentheses = true
1364    ) {
1365        // TODO inject authority
1366        $authority = RequestContext::getMain()->getAuthority();
1367
1368        $revUser = $revRecord->getUser(
1369            $isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
1370            $authority
1371        );
1372        if ( $revUser ) {
1373            $link = self::userLink(
1374                $revUser->getId(),
1375                $revUser->getName(),
1376                false,
1377                [ 'data-mw-revid' => $revRecord->getId() ]
1378            ) . self::userToolLinks(
1379                $revUser->getId(),
1380                $revUser->getName(),
1381                false,
1382                0,
1383                null,
1384                $useParentheses
1385            );
1386        } else {
1387            // User is deleted and we can't (or don't want to) view it
1388            $link = wfMessage( 'rev-deleted-user' )->escaped();
1389        }
1390
1391        if ( $revRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
1392            $class = self::getRevisionDeletedClass( $revRecord );
1393            return ' <span class="' . $class . ' mw-userlink">' . $link . '</span>';
1394        }
1395        return $link;
1396    }
1397
1398    /**
1399     * Helper function to expand local links. Mostly used in action=render
1400     *
1401     * @since 1.38
1402     * @unstable
1403     *
1404     * @param string $html
1405     *
1406     * @return string HTML
1407     */
1408    public static function expandLocalLinks( string $html ) {
1409        return HtmlHelper::modifyElements(
1410            $html,
1411            static function ( SerializerNode $node ): bool {
1412                return $node->name === 'a' && isset( $node->attrs['href'] );
1413            },
1414            static function ( SerializerNode $node ): SerializerNode {
1415                $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
1416                $node->attrs['href'] =
1417                    $urlUtils->expand( $node->attrs['href'], PROTO_RELATIVE ) ?? false;
1418                return $node;
1419            }
1420        );
1421    }
1422
1423    /**
1424     * @param LinkTarget|null $contextTitle
1425     * @param string $target
1426     * @param string &$text
1427     * @return string
1428     */
1429    public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1430        # Valid link forms:
1431        # Foobar -- normal
1432        # :Foobar -- override special treatment of prefix (images, language links)
1433        # /Foobar -- convert to CurrentPage/Foobar
1434        # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1435        # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1436        # ../Foobar -- convert to CurrentPage/Foobar,
1437        #              (from CurrentPage/CurrentSubPage)
1438        # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1439        #              (from CurrentPage/CurrentSubPage)
1440
1441        $ret = $target; # default return value is no change
1442
1443        # Some namespaces don't allow subpages,
1444        # so only perform processing if subpages are allowed
1445        if (
1446            $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
1447            hasSubpages( $contextTitle->getNamespace() )
1448        ) {
1449            $hash = strpos( $target, '#' );
1450            if ( $hash !== false ) {
1451                $suffix = substr( $target, $hash );
1452                $target = substr( $target, 0, $hash );
1453            } else {
1454                $suffix = '';
1455            }
1456            # T9425
1457            $target = trim( $target );
1458            $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
1459                getPrefixedText( $contextTitle );
1460            # Look at the first character
1461            if ( $target != '' && $target[0] === '/' ) {
1462                # / at end means we don't want the slash to be shown
1463                $m = [];
1464                $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1465                if ( $trailingSlashes ) {
1466                    $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1467                } else {
1468                    $noslash = substr( $target, 1 );
1469                }
1470
1471                $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
1472                if ( $text === '' ) {
1473                    $text = $target . $suffix;
1474                } # this might be changed for ugliness reasons
1475            } else {
1476                # check for .. subpage backlinks
1477                $dotdotcount = 0;
1478                $nodotdot = $target;
1479                while ( str_starts_with( $nodotdot, '../' ) ) {
1480                    ++$dotdotcount;
1481                    $nodotdot = substr( $nodotdot, 3 );
1482                }
1483                if ( $dotdotcount > 0 ) {
1484                    $exploded = explode( '/', $contextPrefixedText );
1485                    if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1486                        $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1487                        # / at the end means don't show full path
1488                        if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1489                            $nodotdot = rtrim( $nodotdot, '/' );
1490                            if ( $text === '' ) {
1491                                $text = $nodotdot . $suffix;
1492                            }
1493                        }
1494                        $nodotdot = trim( $nodotdot );
1495                        if ( $nodotdot != '' ) {
1496                            $ret .= '/' . $nodotdot;
1497                        }
1498                        $ret .= $suffix;
1499                    }
1500                }
1501            }
1502        }
1503
1504        return $ret;
1505    }
1506
1507    /**
1508     * @since 1.16.3
1509     * @param int $size
1510     * @return string
1511     */
1512    public static function formatRevisionSize( $size ) {
1513        if ( $size == 0 ) {
1514            $stxt = wfMessage( 'historyempty' )->escaped();
1515        } else {
1516            $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1517        }
1518        return "<span class=\"history-size mw-diff-bytes\" data-mw-bytes=\"$size\">$stxt</span>";
1519    }
1520
1521    /**
1522     * Split a link trail, return the "inside" portion and the remainder of the trail
1523     * as a two-element array
1524     * @param string $trail
1525     * @return string[]
1526     */
1527    public static function splitTrail( $trail ) {
1528        $regex = MediaWikiServices::getInstance()->getContentLanguage()->linkTrail();
1529        $inside = '';
1530        if ( $trail !== '' && preg_match( $regex, $trail, $m ) ) {
1531            [ , $inside, $trail ] = $m;
1532        }
1533        return [ $inside, $trail ];
1534    }
1535
1536    /**
1537     * Generate a rollback link for a given revision.  Currently it's the
1538     * caller's responsibility to ensure that the revision is the top one. If
1539     * it's not, of course, the user will get an error message.
1540     *
1541     * If the calling page is called with the parameter &bot=1, all rollback
1542     * links also get that parameter. It causes the edit itself and the rollback
1543     * to be marked as "bot" edits. Bot edits are hidden by default from recent
1544     * changes, so this allows sysops to combat a busy vandal without bothering
1545     * other users.
1546     *
1547     * This function will return the link only in case the revision can be reverted
1548     * (not all revisions are by the same user, and the last revision by a different
1549     * user is visible). Please note that due to performance limitations it might be
1550     * assumed that a user isn't the only contributor of a page while they are, which
1551     * will lead to useless rollback links. Furthermore this won't work if
1552     * $wgShowRollbackEditCount is disabled, so this can only function as an
1553     * additional check.
1554     *
1555     * If the option noBrackets is set the rollback link wont be enclosed in "[]".
1556     *
1557     * @since 1.16.3. $context added in 1.20. $options added in 1.21
1558     *   $rev could be a RevisionRecord since 1.35
1559     *
1560     * @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
1561     *    since 1.35)
1562     * @param IContextSource|null $context Context to use or null for the main context.
1563     * @param array $options
1564     * @return string
1565     */
1566    public static function generateRollback(
1567        RevisionRecord $revRecord,
1568        ?IContextSource $context = null,
1569        $options = []
1570    ) {
1571        $context ??= RequestContext::getMain();
1572
1573        $editCount = self::getRollbackEditCount( $revRecord );
1574        if ( $editCount === false ) {
1575            return '';
1576        }
1577
1578        $inner = self::buildRollbackLink( $revRecord, $context, $editCount );
1579
1580        $services = MediaWikiServices::getInstance();
1581        // Allow extensions to modify the rollback link.
1582        // Abort further execution if the extension wants full control over the link.
1583        if ( !( new HookRunner( $services->getHookContainer() ) )->onLinkerGenerateRollbackLink(
1584            $revRecord, $context, $options, $inner ) ) {
1585            return $inner;
1586        }
1587
1588        if ( !in_array( 'noBrackets', $options, true ) ) {
1589            $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1590        }
1591
1592        if ( $services->getUserOptionsLookup()
1593            ->getBoolOption( $context->getUser(), 'showrollbackconfirmation' )
1594        ) {
1595            $context->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
1596        }
1597
1598        return '<span class="mw-rollback-link">' . $inner . '</span>';
1599    }
1600
1601    /**
1602     * This function will return the number of revisions which a rollback
1603     * would revert and will verify that a revision can be reverted (that
1604     * the user isn't the only contributor and the revision we might
1605     * rollback to isn't deleted). These checks can only function as an
1606     * additional check as this function only checks against the last
1607     * $wgShowRollbackEditCount edits.
1608     *
1609     * Returns null if $wgShowRollbackEditCount is disabled or false if
1610     * the user is the only contributor of the page.
1611     *
1612     * @todo Unused outside of this file - should it be made private?
1613     *
1614     * @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
1615     *    since 1.35)
1616     * @param bool $verify Deprecated since 1.40, has no effect.
1617     * @return int|false|null
1618     */
1619    public static function getRollbackEditCount( RevisionRecord $revRecord, $verify = true ) {
1620        if ( func_num_args() > 1 ) {
1621            wfDeprecated( __METHOD__ . ' with $verify parameter', '1.40' );
1622        }
1623        $showRollbackEditCount = MediaWikiServices::getInstance()->getMainConfig()
1624            ->get( MainConfigNames::ShowRollbackEditCount );
1625
1626        if ( !is_int( $showRollbackEditCount ) || !$showRollbackEditCount > 0 ) {
1627            // Nothing has happened, indicate this by returning 'null'
1628            return null;
1629        }
1630
1631        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1632
1633        // Up to the value of $wgShowRollbackEditCount revisions are counted
1634        $queryBuilder = MediaWikiServices::getInstance()->getRevisionStore()->newSelectQueryBuilder( $dbr );
1635        $res = $queryBuilder->where( [ 'rev_page' => $revRecord->getPageId() ] )
1636            ->useIndex( [ 'revision' => 'rev_page_timestamp' ] )
1637            ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC )
1638            ->limit( $showRollbackEditCount + 1 )
1639            ->caller( __METHOD__ )->fetchResultSet();
1640
1641        $revUser = $revRecord->getUser( RevisionRecord::RAW );
1642        $revUserText = $revUser ? $revUser->getName() : '';
1643
1644        $editCount = 0;
1645        $moreRevs = false;
1646        foreach ( $res as $row ) {
1647            if ( $row->rev_user_text != $revUserText ) {
1648                if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT
1649                    || $row->rev_deleted & RevisionRecord::DELETED_USER
1650                ) {
1651                    // If the user or the text of the revision we might rollback
1652                    // to is deleted in some way we can't rollback. Similar to
1653                    // the checks in WikiPage::commitRollback.
1654                    return false;
1655                }
1656                $moreRevs = true;
1657                break;
1658            }
1659            $editCount++;
1660        }
1661
1662        if ( $editCount <= $showRollbackEditCount && !$moreRevs ) {
1663            // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1664            // and there weren't any other revisions. That means that the current user is the only
1665            // editor, so we can't rollback
1666            return false;
1667        }
1668        return $editCount;
1669    }
1670
1671    /**
1672     * Build a raw rollback link, useful for collections of "tool" links
1673     *
1674     * @since 1.16.3. $context added in 1.20. $editCount added in 1.21
1675     *   $rev could be a RevisionRecord since 1.35
1676     *
1677     * @todo Unused outside of this file - should it be made private?
1678     *
1679     * @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
1680     *    since 1.35)
1681     * @param IContextSource|null $context Context to use or null for the main context.
1682     * @param int|false|null $editCount Number of edits that would be reverted
1683     * @return string HTML fragment
1684     */
1685    public static function buildRollbackLink(
1686        RevisionRecord $revRecord,
1687        ?IContextSource $context = null,
1688        $editCount = false
1689    ) {
1690        $config = MediaWikiServices::getInstance()->getMainConfig();
1691        $showRollbackEditCount = $config->get( MainConfigNames::ShowRollbackEditCount );
1692        $miserMode = $config->get( MainConfigNames::MiserMode );
1693        // To config which pages are affected by miser mode
1694        $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1695
1696        $context ??= RequestContext::getMain();
1697
1698        $title = $revRecord->getPageAsLinkTarget();
1699        $revUser = $revRecord->getUser();
1700        $revUserText = $revUser ? $revUser->getName() : '';
1701
1702        $query = [
1703            'action' => 'rollback',
1704            'from' => $revUserText,
1705            'token' => $context->getUser()->getEditToken( 'rollback' ),
1706        ];
1707
1708        $attrs = [
1709            'data-mw' => 'interface',
1710            'title' => $context->msg( 'tooltip-rollback' )->text()
1711        ];
1712
1713        $options = [ 'known', 'noclasses' ];
1714
1715        if ( $context->getRequest()->getBool( 'bot' ) ) {
1716            // T17999
1717            $query['hidediff'] = '1';
1718            $query['bot'] = '1';
1719        }
1720
1721        if ( $miserMode ) {
1722            foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1723                if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1724                    $showRollbackEditCount = false;
1725                    break;
1726                }
1727            }
1728        }
1729
1730        // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message
1731        $msg = [ 'rollbacklink' ];
1732        if ( is_int( $showRollbackEditCount ) && $showRollbackEditCount > 0 ) {
1733            if ( !is_numeric( $editCount ) ) {
1734                $editCount = self::getRollbackEditCount( $revRecord );
1735            }
1736
1737            if ( $editCount > $showRollbackEditCount ) {
1738                $msg = [ 'rollbacklinkcount-morethan', Message::numParam( $showRollbackEditCount ) ];
1739            } elseif ( $editCount ) {
1740                $msg = [ 'rollbacklinkcount', Message::numParam( $editCount ) ];
1741            }
1742        }
1743
1744        $html = $context->msg( ...$msg )->parse();
1745        return self::link( $title, $html, $attrs, $query, $options );
1746    }
1747
1748    /**
1749     * Returns HTML for the "hidden categories on this page" list.
1750     *
1751     * @since 1.16.3
1752     * @param array $hiddencats Array of hidden categories
1753     *    from {@link WikiPage::getHiddenCategories} or similar
1754     * @return string HTML output
1755     */
1756    public static function formatHiddenCategories( $hiddencats ) {
1757        $outText = '';
1758        if ( count( $hiddencats ) > 0 ) {
1759            # Construct the HTML
1760            $outText = '<div class="mw-hiddenCategoriesExplanation">';
1761            $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
1762            $outText .= "</div><ul>\n";
1763
1764            foreach ( $hiddencats as $titleObj ) {
1765                # If it's hidden, it must exist - no need to check with a LinkBatch
1766                $outText .= '<li>'
1767                    . self::link( $titleObj, null, [], [], 'known' )
1768                    . "</li>\n";
1769            }
1770            $outText .= '</ul>';
1771        }
1772        return $outText;
1773    }
1774
1775    /**
1776     * @return ContextSource
1777     */
1778    private static function getContextFromMain() {
1779        $context = RequestContext::getMain();
1780        $context = new DerivativeContext( $context );
1781        return $context;
1782    }
1783
1784    /**
1785     * Given the id of an interface element, constructs the appropriate title
1786     * attribute from the system messages.  (Note, this is usually the id but
1787     * isn't always, because sometimes the accesskey needs to go on a different
1788     * element than the id, for reverse-compatibility, etc.)
1789     *
1790     * @since 1.16.3 $msgParams added in 1.27
1791     * @param string $name Id of the element, minus prefixes.
1792     * @param string|array|null $options Null, string or array with some of the following options:
1793     *   - 'withaccess' to add an access-key hint
1794     *   - 'nonexisting' to add an accessibility hint that page does not exist
1795     * @param array $msgParams Parameters to pass to the message
1796     * @param MessageLocalizer|null $localizer
1797     *
1798     * @return string|false Contents of the title attribute (which you must HTML-
1799     *   escape), or false for no title attribute
1800     */
1801    public static function titleAttrib( $name, $options = null, array $msgParams = [], $localizer = null ) {
1802        if ( !$localizer ) {
1803            $localizer = self::getContextFromMain();
1804        }
1805        $message = $localizer->msg( "tooltip-$name", $msgParams );
1806        // Set a default tooltip for subject namespace tabs if that hasn't
1807        // been defined. See T22126
1808        if ( !$message->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1809            $message = $localizer->msg( 'tooltip-ca-nstab' );
1810        }
1811
1812        if ( $message->isDisabled() ) {
1813            $tooltip = false;
1814        } else {
1815            $tooltip = $message->text();
1816            # Compatibility: formerly some tooltips had [alt-.] hardcoded
1817            $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
1818        }
1819
1820        $options = (array)$options;
1821
1822        if ( in_array( 'nonexisting', $options ) ) {
1823            $tooltip = $localizer->msg( 'red-link-title', $tooltip ?: '' )->text();
1824        }
1825        if ( in_array( 'withaccess', $options ) ) {
1826            $accesskey = self::accesskey( $name, $localizer );
1827            if ( $accesskey !== false ) {
1828                // Should be build the same as in jquery.accessKeyLabel.js
1829                if ( $tooltip === false || $tooltip === '' ) {
1830                    $tooltip = $localizer->msg( 'brackets', $accesskey )->text();
1831                } else {
1832                    $tooltip .= $localizer->msg( 'word-separator' )->text();
1833                    $tooltip .= $localizer->msg( 'brackets', $accesskey )->text();
1834                }
1835            }
1836        }
1837
1838        return $tooltip;
1839    }
1840
1841    /** @var (string|false)[] */
1842    public static $accesskeycache;
1843
1844    /**
1845     * Given the id of an interface element, constructs the appropriate
1846     * accesskey attribute from the system messages.  (Note, this is usually
1847     * the id but isn't always, because sometimes the accesskey needs to go on
1848     * a different element than the id, for reverse-compatibility, etc.)
1849     *
1850     * @since 1.16.3
1851     * @param string $name Id of the element, minus prefixes.
1852     * @param MessageLocalizer|null $localizer
1853     * @return string|false Contents of the accesskey attribute (which you must HTML-
1854     *   escape), or false for no accesskey attribute
1855     */
1856    public static function accesskey( $name, $localizer = null ) {
1857        if ( !isset( self::$accesskeycache[$name] ) ) {
1858            if ( !$localizer ) {
1859                $localizer = self::getContextFromMain();
1860            }
1861            $msg = $localizer->msg( "accesskey-$name" );
1862            // Set a default accesskey for subject namespace tabs if an
1863            // accesskey has not been defined. See T22126
1864            if ( !$msg->exists() && str_starts_with( $name, 'ca-nstab-' ) ) {
1865                $msg = $localizer->msg( 'accesskey-ca-nstab' );
1866            }
1867            self::$accesskeycache[$name] = $msg->isDisabled() ? false : $msg->plain();
1868        }
1869        return self::$accesskeycache[$name];
1870    }
1871
1872    /**
1873     * Get a revision-deletion link, or disabled link, or nothing, depending
1874     * on user permissions & the settings on the revision.
1875     *
1876     * Will use forward-compatible revision ID in the Special:RevDelete link
1877     * if possible, otherwise the timestamp-based ID which may break after
1878     * undeletion.
1879     *
1880     * @param Authority $performer
1881     * @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
1882     *    since 1.35)
1883     * @param LinkTarget $title
1884     * @return string HTML fragment
1885     */
1886    public static function getRevDeleteLink(
1887        Authority $performer,
1888        RevisionRecord $revRecord,
1889        LinkTarget $title
1890    ) {
1891        $canHide = $performer->isAllowed( 'deleterevision' );
1892        $canHideHistory = $performer->isAllowed( 'deletedhistory' );
1893        if ( !$canHide && !( $revRecord->getVisibility() && $canHideHistory ) ) {
1894            return '';
1895        }
1896
1897        if ( !$revRecord->userCan( RevisionRecord::DELETED_RESTRICTED, $performer ) ) {
1898            return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
1899        }
1900        $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
1901            getPrefixedDBkey( $title );
1902        if ( $revRecord->getId() ) {
1903            // RevDelete links using revision ID are stable across
1904            // page deletion and undeletion; use when possible.
1905            $query = [
1906                'type' => 'revision',
1907                'target' => $prefixedDbKey,
1908                'ids' => $revRecord->getId()
1909            ];
1910        } else {
1911            // Older deleted entries didn't save a revision ID.
1912            // We have to refer to these by timestamp, ick!
1913            $query = [
1914                'type' => 'archive',
1915                'target' => $prefixedDbKey,
1916                'ids' => $revRecord->getTimestamp()
1917            ];
1918        }
1919        return self::revDeleteLink(
1920            $query,
1921            $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ),
1922            $canHide
1923        );
1924    }
1925
1926    /**
1927     * Creates a (show/hide) link for deleting revisions/log entries
1928     *
1929     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
1930     *
1931     * @param array $query Query parameters to be passed to link()
1932     * @param bool $restricted Set to true to use a "<strong>" instead of a "<span>"
1933     * @param bool $delete Set to true to use (show/hide) rather than (show)
1934     *
1935     * @return string HTML "<a>" link to Special:Revisiondelete, wrapped in a
1936     * span to allow for customization of appearance with CSS
1937     */
1938    public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
1939        $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
1940        $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1941        $html = wfMessage( $msgKey )->escaped();
1942        $tag = $restricted ? 'strong' : 'span';
1943        $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
1944        return Html::rawElement(
1945            $tag,
1946            [ 'class' => 'mw-revdelundel-link' ],
1947            wfMessage( 'parentheses' )->rawParams( $link )->escaped()
1948        );
1949    }
1950
1951    /**
1952     * Creates a dead (show/hide) link for deleting revisions/log entries
1953     *
1954     * This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
1955     *
1956     * @since 1.16.3
1957     * @param bool $delete Set to true to use (show/hide) rather than (show)
1958     *
1959     * @return string HTML text wrapped in a span to allow for customization
1960     * of appearance with CSS
1961     */
1962    public static function revDeleteLinkDisabled( $delete = true ) {
1963        $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
1964        $html = wfMessage( $msgKey )->escaped();
1965        $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
1966        return Html::rawElement( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
1967    }
1968
1969    /**
1970     * Returns the attributes for the tooltip and access key.
1971     *
1972     * @since 1.16.3. $msgParams introduced in 1.27
1973     * @param string $name
1974     * @param array $msgParams Params for constructing the message
1975     * @param string|array|null $options Options to be passed to titleAttrib.
1976     * @param MessageLocalizer|null $localizer
1977     *
1978     * @see Linker::titleAttrib for what options could be passed to $options.
1979     *
1980     * @return array
1981     */
1982    public static function tooltipAndAccesskeyAttribs(
1983        $name,
1984        array $msgParams = [],
1985        $options = null,
1986        $localizer = null
1987    ) {
1988        $options = (array)$options;
1989        $options[] = 'withaccess';
1990
1991        // Get optional parameters from global context if any missing.
1992        if ( !$localizer ) {
1993            $localizer = self::getContextFromMain();
1994        }
1995
1996        $attribs = [
1997            'title' => self::titleAttrib( $name, $options, $msgParams, $localizer ),
1998            'accesskey' => self::accesskey( $name, $localizer )
1999        ];
2000        if ( $attribs['title'] === false ) {
2001            unset( $attribs['title'] );
2002        }
2003        if ( $attribs['accesskey'] === false ) {
2004            unset( $attribs['accesskey'] );
2005        }
2006        return $attribs;
2007    }
2008
2009    /**
2010     * Returns raw bits of HTML, use titleAttrib()
2011     * @since 1.16.3
2012     * @param string $name
2013     * @param array|null $options
2014     * @return null|string
2015     */
2016    public static function tooltip( $name, $options = null ) {
2017        $tooltip = self::titleAttrib( $name, $options );
2018        if ( $tooltip === false ) {
2019            return '';
2020        }
2021        return Html::expandAttributes( [
2022            'title' => $tooltip
2023        ] );
2024    }
2025
2026}