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