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;
78 * Whether to run the legacy Linker hooks
82 private $runLegacyBeginHook = true;
85 * @param TitleFormatter $titleFormatter
86 * @param LinkCache $linkCache
87 * @param NamespaceInfo $nsInfo
89 public function __construct(
90 TitleFormatter
$titleFormatter, LinkCache
$linkCache, NamespaceInfo
$nsInfo
92 $this->titleFormatter
= $titleFormatter;
93 $this->linkCache
= $linkCache;
94 $this->nsInfo
= $nsInfo;
100 public function setForceArticlePath( $force ) {
101 $this->forceArticlePath
= $force;
107 public function getForceArticlePath() {
108 return $this->forceArticlePath
;
112 * @param string|bool|int $expand A PROTO_* constant or false
114 public function setExpandURLs( $expand ) {
115 $this->expandUrls
= $expand;
119 * @return string|bool|int a PROTO_* constant or false
121 public function getExpandURLs() {
122 return $this->expandUrls
;
126 * @param int $threshold
128 public function setStubThreshold( $threshold ) {
129 $this->stubThreshold
= $threshold;
135 public function getStubThreshold() {
136 return $this->stubThreshold
;
142 public function setRunLegacyBeginHook( $run ) {
143 $this->runLegacyBeginHook
= $run;
147 * @param LinkTarget $target
148 * @param string|HtmlArmor|null $text
149 * @param array $extraAttribs
150 * @param array $query
153 public function makeLink(
154 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
156 $title = Title
::newFromLinkTarget( $target );
157 if ( $title->isKnown() ) {
158 return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
160 return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
165 * Get the options in the legacy format
167 * @param bool $isKnown Whether the link is known or broken
170 private function getLegacyOptions( $isKnown ) {
171 $options = [ 'stubThreshold' => $this->stubThreshold
];
172 if ( $this->forceArticlePath
) {
173 $options[] = 'forcearticlepath';
175 if ( $this->expandUrls
=== PROTO_HTTP
) {
177 } elseif ( $this->expandUrls
=== PROTO_HTTPS
) {
178 $options[] = 'https';
181 $options[] = $isKnown ?
'known' : 'broken';
186 private function runBeginHook( LinkTarget
$target, &$text, &$extraAttribs, &$query, $isKnown ) {
188 if ( !Hooks
::run( 'HtmlPageLinkRendererBegin',
189 [ $this, $target, &$text, &$extraAttribs, &$query, &$ret ] )
194 // Now run the legacy hook
195 return $this->runLegacyBeginHook( $target, $text, $extraAttribs, $query, $isKnown );
198 private function runLegacyBeginHook( LinkTarget
$target, &$text, &$extraAttribs, &$query,
201 if ( !$this->runLegacyBeginHook ||
!Hooks
::isRegistered( 'LinkBegin' ) ) {
202 // Disabled, or nothing registered
206 $realOptions = $options = $this->getLegacyOptions( $isKnown );
208 $dummy = new DummyLinker();
209 $title = Title
::newFromLinkTarget( $target );
210 if ( $text !== null ) {
211 $realHtml = $html = HtmlArmor
::getHtml( $text );
213 $realHtml = $html = null;
215 if ( !Hooks
::run( 'LinkBegin',
216 [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ], '1.28' )
221 if ( $html !== null && $html !== $realHtml ) {
222 // &$html was modified, so re-armor it as $text
223 $text = new HtmlArmor( $html );
226 // Check if they changed any of the options, hopefully not!
227 if ( $options !== $realOptions ) {
228 $factory = MediaWikiServices
::getInstance()->getLinkRendererFactory();
229 // They did, so create a separate instance and have that take over the rest
230 $newRenderer = $factory->createFromLegacyOptions( $options );
231 // Don't recurse the hook...
232 $newRenderer->setRunLegacyBeginHook( false );
233 if ( in_array( 'known', $options, true ) ) {
234 return $newRenderer->makeKnownLink( $title, $text, $extraAttribs, $query );
235 } elseif ( in_array( 'broken', $options, true ) ) {
236 return $newRenderer->makeBrokenLink( $title, $text, $extraAttribs, $query );
238 return $newRenderer->makeLink( $title, $text, $extraAttribs, $query );
246 * If you have already looked up the proper CSS classes using LinkRenderer::getLinkClasses()
247 * or some other method, use this to avoid looking it up again.
249 * @param LinkTarget $target
250 * @param string|HtmlArmor|null $text
251 * @param string $classes CSS classes to add
252 * @param array $extraAttribs
253 * @param array $query
256 public function makePreloadedLink(
257 LinkTarget
$target, $text = null, $classes = '', array $extraAttribs = [], array $query = []
260 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, true );
261 if ( $ret !== null ) {
264 $target = $this->normalizeTarget( $target );
265 $url = $this->getLinkURL( $target, $query );
266 $attribs = [ 'class' => $classes ];
267 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
268 if ( $prefixedText !== '' ) {
269 $attribs['title'] = $prefixedText;
274 ] +
$this->mergeAttribs( $attribs, $extraAttribs );
276 if ( $text === null ) {
277 $text = $this->getLinkText( $target );
280 return $this->buildAElement( $target, $text, $attribs, true );
284 * @param LinkTarget $target
285 * @param string|HtmlArmor|null $text
286 * @param array $extraAttribs
287 * @param array $query
290 public function makeKnownLink(
291 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
294 if ( $target->isExternal() ) {
295 $classes[] = 'extiw';
297 $colour = $this->getLinkClasses( $target );
298 if ( $colour !== '' ) {
299 $classes[] = $colour;
302 return $this->makePreloadedLink(
305 implode( ' ', $classes ),
312 * @param LinkTarget $target
313 * @param string|HtmlArmor|null $text
314 * @param array $extraAttribs
315 * @param array $query
318 public function makeBrokenLink(
319 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
322 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, false );
323 if ( $ret !== null ) {
327 # We don't want to include fragments for broken links, because they
328 # generally make no sense.
329 if ( $target->hasFragment() ) {
330 $target = $target->createFragmentTarget( '' );
332 $target = $this->normalizeTarget( $target );
334 if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL
) {
335 $query['action'] = 'edit';
336 $query['redlink'] = '1';
339 $url = $this->getLinkURL( $target, $query );
340 $attribs = [ 'class' => 'new' ];
341 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
342 if ( $prefixedText !== '' ) {
343 // This ends up in parser cache!
344 $attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
345 ->inContentLanguage()
351 ] +
$this->mergeAttribs( $attribs, $extraAttribs );
353 if ( $text === null ) {
354 $text = $this->getLinkText( $target );
357 return $this->buildAElement( $target, $text, $attribs, false );
361 * Builds the final <a> element
363 * @param LinkTarget $target
364 * @param string|HtmlArmor $text
365 * @param array $attribs
366 * @param bool $isKnown
367 * @return null|string
369 private function buildAElement( LinkTarget
$target, $text, array $attribs, $isKnown ) {
371 if ( !Hooks
::run( 'HtmlPageLinkRendererEnd',
372 [ $this, $target, $isKnown, &$text, &$attribs, &$ret ] )
377 $html = HtmlArmor
::getHtml( $text );
380 if ( Hooks
::isRegistered( 'LinkEnd' ) ) {
381 $dummy = new DummyLinker();
382 $title = Title
::newFromLinkTarget( $target );
383 $options = $this->getLegacyOptions( $isKnown );
384 if ( !Hooks
::run( 'LinkEnd',
385 [ $dummy, $title, $options, &$html, &$attribs, &$ret ], '1.28' )
391 return Html
::rawElement( 'a', $attribs, $html );
395 * @param LinkTarget $target
396 * @return string non-escaped text
398 private function getLinkText( LinkTarget
$target ) {
399 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
400 // If the target is just a fragment, with no title, we return the fragment
401 // text. Otherwise, we return the title text itself.
402 if ( $prefixedText === '' && $target->hasFragment() ) {
403 return $target->getFragment();
406 return $prefixedText;
409 private function getLinkURL( LinkTarget
$target, array $query = [] ) {
410 // TODO: Use a LinkTargetResolver service instead of Title
411 $title = Title
::newFromLinkTarget( $target );
412 if ( $this->forceArticlePath
) {
418 $url = $title->getLinkURL( $query, false, $this->expandUrls
);
420 if ( $this->forceArticlePath
&& $realQuery ) {
421 $url = wfAppendQuery( $url, $realQuery );
428 * Normalizes the provided target
430 * @todo move the code from Linker actually here
431 * @param LinkTarget $target
434 private function normalizeTarget( LinkTarget
$target ) {
435 return Linker
::normaliseSpecialPage( $target );
439 * Merges two sets of attributes
441 * @param array $defaults
442 * @param array $attribs
446 private function mergeAttribs( $defaults, $attribs ) {
450 # Merge the custom attribs with the default ones, and iterate
451 # over that, deleting all "false" attributes.
453 $merged = Sanitizer
::mergeAttributes( $defaults, $attribs );
454 foreach ( $merged as $key => $val ) {
455 # A false value suppresses the attribute
456 if ( $val !== false ) {
464 * Return the CSS classes of a known link
466 * @param LinkTarget $target
467 * @return string CSS class
469 public function getLinkClasses( LinkTarget
$target ) {
470 // Make sure the target is in the cache
471 $id = $this->linkCache
->addLinkObj( $target );
477 if ( $this->linkCache
->getGoodLinkFieldObj( $target, 'redirect' ) ) {
479 return 'mw-redirect';
481 $this->stubThreshold
> 0 && $this->nsInfo
->isContent( $target->getNamespace() ) &&
482 $this->linkCache
->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold