3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
19 * @author Kunal Mehta <legoktm@member.fsf.org>
21 namespace MediaWiki\Linker
;
29 use MediaWiki\MediaWikiServices
;
36 * Class that generates HTML <a> links for pages.
38 * @see https://www.mediawiki.org/wiki/Manual:LinkRenderer
44 * Whether to force the pretty article path
48 private $forceArticlePath = false;
51 * A PROTO_* constant or false
53 * @var string|bool|int
55 private $expandUrls = false;
60 private $stubThreshold = 0;
65 private $titleFormatter;
73 * Whether to run the legacy Linker hooks
77 private $runLegacyBeginHook = true;
80 * @param TitleFormatter $titleFormatter
81 * @param LinkCache $linkCache
83 public function __construct( TitleFormatter
$titleFormatter, LinkCache
$linkCache ) {
84 $this->titleFormatter
= $titleFormatter;
85 $this->linkCache
= $linkCache;
91 public function setForceArticlePath( $force ) {
92 $this->forceArticlePath
= $force;
98 public function getForceArticlePath() {
99 return $this->forceArticlePath
;
103 * @param string|bool|int $expand A PROTO_* constant or false
105 public function setExpandURLs( $expand ) {
106 $this->expandUrls
= $expand;
110 * @return string|bool|int a PROTO_* constant or false
112 public function getExpandURLs() {
113 return $this->expandUrls
;
117 * @param int $threshold
119 public function setStubThreshold( $threshold ) {
120 $this->stubThreshold
= $threshold;
126 public function getStubThreshold() {
127 return $this->stubThreshold
;
133 public function setRunLegacyBeginHook( $run ) {
134 $this->runLegacyBeginHook
= $run;
138 * @param LinkTarget $target
139 * @param string|HtmlArmor|null $text
140 * @param array $extraAttribs
141 * @param array $query
144 public function makeLink(
145 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
147 $title = Title
::newFromLinkTarget( $target );
148 if ( $title->isKnown() ) {
149 return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
151 return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
156 * Get the options in the legacy format
158 * @param bool $isKnown Whether the link is known or broken
161 private function getLegacyOptions( $isKnown ) {
162 $options = [ 'stubThreshold' => $this->stubThreshold
];
163 if ( $this->forceArticlePath
) {
164 $options[] = 'forcearticlepath';
166 if ( $this->expandUrls
=== PROTO_HTTP
) {
168 } elseif ( $this->expandUrls
=== PROTO_HTTPS
) {
169 $options[] = 'https';
172 $options[] = $isKnown ?
'known' : 'broken';
177 private function runBeginHook( LinkTarget
$target, &$text, &$extraAttribs, &$query, $isKnown ) {
179 if ( !Hooks
::run( 'HtmlPageLinkRendererBegin',
180 [ $this, $target, &$text, &$extraAttribs, &$query, &$ret ] )
185 // Now run the legacy hook
186 return $this->runLegacyBeginHook( $target, $text, $extraAttribs, $query, $isKnown );
189 private function runLegacyBeginHook( LinkTarget
$target, &$text, &$extraAttribs, &$query,
192 if ( !$this->runLegacyBeginHook ||
!Hooks
::isRegistered( 'LinkBegin' ) ) {
193 // Disabled, or nothing registered
197 $realOptions = $options = $this->getLegacyOptions( $isKnown );
199 $dummy = new DummyLinker();
200 $title = Title
::newFromLinkTarget( $target );
201 if ( $text !== null ) {
202 $realHtml = $html = HtmlArmor
::getHtml( $text );
204 $realHtml = $html = null;
206 if ( !Hooks
::run( 'LinkBegin',
207 [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ] )
212 if ( $html !== null && $html !== $realHtml ) {
213 // &$html was modified, so re-armor it as $text
214 $text = new HtmlArmor( $html );
217 // Check if they changed any of the options, hopefully not!
218 if ( $options !== $realOptions ) {
219 $factory = MediaWikiServices
::getInstance()->getLinkRendererFactory();
220 // They did, so create a separate instance and have that take over the rest
221 $newRenderer = $factory->createFromLegacyOptions( $options );
222 // Don't recurse the hook...
223 $newRenderer->setRunLegacyBeginHook( false );
224 if ( in_array( 'known', $options, true ) ) {
225 return $newRenderer->makeKnownLink( $title, $text, $extraAttribs, $query );
226 } elseif ( in_array( 'broken', $options, true ) ) {
227 return $newRenderer->makeBrokenLink( $title, $text, $extraAttribs, $query );
229 return $newRenderer->makeLink( $title, $text, $extraAttribs, $query );
237 * If you have already looked up the proper CSS classes using LinkRenderer::getLinkClasses()
238 * or some other method, use this to avoid looking it up again.
240 * @param LinkTarget $target
241 * @param string|HtmlArmor|null $text
242 * @param string $classes CSS classes to add
243 * @param array $extraAttribs
244 * @param array $query
247 public function makePreloadedLink(
248 LinkTarget
$target, $text = null, $classes, array $extraAttribs = [], array $query = []
251 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, true );
252 if ( $ret !== null ) {
255 $target = $this->normalizeTarget( $target );
256 $url = $this->getLinkURL( $target, $query );
257 $attribs = [ 'class' => $classes ];
258 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
259 if ( $prefixedText !== '' ) {
260 $attribs['title'] = $prefixedText;
265 ] +
$this->mergeAttribs( $attribs, $extraAttribs );
267 if ( $text === null ) {
268 $text = $this->getLinkText( $target );
271 return $this->buildAElement( $target, $text, $attribs, true );
275 * @param LinkTarget $target
276 * @param string|HtmlArmor|null $text
277 * @param array $extraAttribs
278 * @param array $query
281 public function makeKnownLink(
282 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
285 if ( $target->isExternal() ) {
286 $classes[] = 'extiw';
288 $colour = $this->getLinkClasses( $target );
289 if ( $colour !== '' ) {
290 $classes[] = $colour;
293 return $this->makePreloadedLink(
296 $classes ?
implode( ' ', $classes ) : '',
303 * @param LinkTarget $target
304 * @param string|HtmlArmor|null $text
305 * @param array $extraAttribs
306 * @param array $query
309 public function makeBrokenLink(
310 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
313 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, false );
314 if ( $ret !== null ) {
318 # We don't want to include fragments for broken links, because they
319 # generally make no sense.
320 if ( $target->hasFragment() ) {
321 $target = $target->createFragmentTarget( '' );
323 $target = $this->normalizeTarget( $target );
325 if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL
) {
326 $query['action'] = 'edit';
327 $query['redlink'] = '1';
330 $url = $this->getLinkURL( $target, $query );
331 $attribs = [ 'class' => 'new' ];
332 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
333 if ( $prefixedText !== '' ) {
334 // This ends up in parser cache!
335 $attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
336 ->inContentLanguage()
342 ] +
$this->mergeAttribs( $attribs, $extraAttribs );
344 if ( $text === null ) {
345 $text = $this->getLinkText( $target );
348 return $this->buildAElement( $target, $text, $attribs, false );
352 * Builds the final <a> element
354 * @param LinkTarget $target
355 * @param string|HtmlArmor $text
356 * @param array $attribs
357 * @param bool $isKnown
358 * @return null|string
360 private function buildAElement( LinkTarget
$target, $text, array $attribs, $isKnown ) {
362 if ( !Hooks
::run( 'HtmlPageLinkRendererEnd',
363 [ $this, $target, $isKnown, &$text, &$attribs, &$ret ] )
368 $html = HtmlArmor
::getHtml( $text );
371 if ( Hooks
::isRegistered( 'LinkEnd' ) ) {
372 $dummy = new DummyLinker();
373 $title = Title
::newFromLinkTarget( $target );
374 $options = $this->getLegacyOptions( $isKnown );
375 if ( !Hooks
::run( 'LinkEnd',
376 [ $dummy, $title, $options, &$html, &$attribs, &$ret ] )
382 return Html
::rawElement( 'a', $attribs, $html );
386 * @param LinkTarget $target
387 * @return string non-escaped text
389 private function getLinkText( LinkTarget
$target ) {
390 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
391 // If the target is just a fragment, with no title, we return the fragment
392 // text. Otherwise, we return the title text itself.
393 if ( $prefixedText === '' && $target->hasFragment() ) {
394 return $target->getFragment();
397 return $prefixedText;
400 private function getLinkURL( LinkTarget
$target, array $query = [] ) {
401 // TODO: Use a LinkTargetResolver service instead of Title
402 $title = Title
::newFromLinkTarget( $target );
403 if ( $this->forceArticlePath
) {
409 $url = $title->getLinkURL( $query, false, $this->expandUrls
);
411 if ( $this->forceArticlePath
&& $realQuery ) {
412 $url = wfAppendQuery( $url, $realQuery );
419 * Normalizes the provided target
421 * @todo move the code from Linker actually here
422 * @param LinkTarget $target
425 private function normalizeTarget( LinkTarget
$target ) {
426 return Linker
::normaliseSpecialPage( $target );
430 * Merges two sets of attributes
432 * @param array $defaults
433 * @param array $attribs
437 private function mergeAttribs( $defaults, $attribs ) {
441 # Merge the custom attribs with the default ones, and iterate
442 # over that, deleting all "false" attributes.
444 $merged = Sanitizer
::mergeAttributes( $defaults, $attribs );
445 foreach ( $merged as $key => $val ) {
446 # A false value suppresses the attribute
447 if ( $val !== false ) {
455 * Return the CSS classes of a known link
457 * @param LinkTarget $target
458 * @return string CSS class
460 public function getLinkClasses( LinkTarget
$target ) {
461 // Make sure the target is in the cache
462 $id = $this->linkCache
->addLinkObj( $target );
468 if ( $this->linkCache
->getGoodLinkFieldObj( $target, 'redirect' ) ) {
470 return 'mw-redirect';
471 } elseif ( $this->stubThreshold
> 0 && MWNamespace
::isContent( $target->getNamespace() )
472 && $this->linkCache
->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold