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
20 * @author Kunal Mehta <legoktm@member.fsf.org>
22 namespace MediaWiki\Linker
;
29 use MediaWiki\MediaWikiServices
;
35 * Class that generates HTML <a> links for pages.
42 * Whether to force the pretty article path
46 private $forceArticlePath = false;
49 * A PROTO_* constant or false
51 * @var string|bool|int
53 private $expandUrls = false;
56 * Whether extra classes should be added
60 private $noClasses = false;
65 private $stubThreshold = 0;
70 private $titleFormatter;
73 * Whether to run the legacy Linker hooks
77 private $runLegacyBeginHook = true;
80 * @param TitleFormatter $titleFormatter
82 public function __construct( TitleFormatter
$titleFormatter ) {
83 $this->titleFormatter
= $titleFormatter;
89 public function setForceArticlePath( $force ) {
90 $this->forceArticlePath
= $force;
96 public function getForceArticlePath() {
97 return $this->forceArticlePath
;
101 * @param string|bool|int $expand A PROTO_* constant or false
103 public function setExpandURLs( $expand ) {
104 $this->expandUrls
= $expand;
108 * @return string|bool|int a PROTO_* constant or false
110 public function getExpandURLs() {
111 return $this->expandUrls
;
117 public function setNoClasses( $no ) {
118 $this->noClasses
= $no;
124 public function getNoClasses() {
125 return $this->noClasses
;
129 * @param int $threshold
131 public function setStubThreshold( $threshold ) {
132 $this->stubThreshold
= $threshold;
138 public function getStubThreshold() {
139 return $this->stubThreshold
;
145 public function setRunLegacyBeginHook( $run ) {
146 $this->runLegacyBeginHook
= $run;
150 * @param LinkTarget $target
151 * @param string|HtmlArmor|null $text
152 * @param array $extraAttribs
153 * @param array $query
156 public function makeLink(
157 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
159 $title = Title
::newFromLinkTarget( $target );
160 if ( $title->isKnown() ) {
161 return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
163 return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
168 * Get the options in the legacy format
170 * @param bool $isKnown Whether the link is known or broken
173 private function getLegacyOptions( $isKnown ) {
174 $options = [ 'stubThreshold' => $this->stubThreshold
];
175 if ( $this->noClasses
) {
176 $options[] = 'noclasses';
178 if ( $this->forceArticlePath
) {
179 $options[] = 'forcearticlepath';
181 if ( $this->expandUrls
=== PROTO_HTTP
) {
183 } elseif ( $this->expandUrls
=== PROTO_HTTPS
) {
184 $options[] = 'https';
187 $options[] = $isKnown ?
'known' : 'broken';
192 private function runBeginHook( LinkTarget
$target, &$text, &$extraAttribs, &$query, $isKnown ) {
194 if ( !Hooks
::run( 'HtmlPageLinkRendererBegin',
195 [ $this, $target, &$text, &$extraAttribs, &$query, &$ret ] )
200 // Now run the legacy hook
201 return $this->runLegacyBeginHook( $target, $text, $extraAttribs, $query, $isKnown );
204 private function runLegacyBeginHook( LinkTarget
$target, &$text, &$extraAttribs, &$query,
207 if ( !$this->runLegacyBeginHook ||
!Hooks
::isRegistered( 'LinkBegin' ) ) {
208 // Disabled, or nothing registered
212 $realOptions = $options = $this->getLegacyOptions( $isKnown );
214 $dummy = new DummyLinker();
215 $title = Title
::newFromLinkTarget( $target );
216 $realHtml = $html = HtmlArmor
::getHtml( $text );
217 if ( !Hooks
::run( 'LinkBegin',
218 [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ] )
223 if ( $html !== null && $html !== $realHtml ) {
224 // &$html was modified, so re-armor it as $text
225 $text = new HtmlArmor( $html );
228 // Check if they changed any of the options, hopefully not!
229 if ( $options !== $realOptions ) {
230 $factory = MediaWikiServices
::getInstance()->getLinkRendererFactory();
231 // They did, so create a separate instance and have that take over the rest
232 $newRenderer = $factory->createFromLegacyOptions( $options );
233 // Don't recurse the hook...
234 $newRenderer->setRunLegacyBeginHook( false );
235 if ( in_array( 'known', $options, true ) ) {
236 return $newRenderer->makeKnownLink( $title, $text, $extraAttribs, $query );
237 } elseif ( in_array( 'broken', $options, true ) ) {
238 return $newRenderer->makeBrokenLink( $title, $text, $extraAttribs, $query );
240 return $newRenderer->makeLink( $title, $text, $extraAttribs, $query );
248 * @param LinkTarget $target
249 * @param string|HtmlArmor|null $text
250 * @param array $extraAttribs
251 * @param array $query
254 public function makeKnownLink(
255 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
258 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, true );
259 if ( $ret !== null ) {
262 $target = $this->normalizeTarget( $target );
263 $url = $this->getLinkURL( $target, $query );
265 if ( !$this->noClasses
) {
267 if ( $target->isExternal() ) {
268 $classes[] = 'extiw';
270 $title = Title
::newFromLinkTarget( $target );
271 $colour = Linker
::getLinkColour( $title, $this->stubThreshold
);
272 if ( $colour !== '' ) {
273 $classes[] = $colour;
276 $attribs['class'] = implode( ' ', $classes );
280 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
281 if ( $prefixedText !== '' ) {
282 $attribs['title'] = $prefixedText;
287 ] +
$this->mergeAttribs( $attribs, $extraAttribs );
289 if ( $text === null ) {
290 $text = $this->getLinkText( $target );
293 return $this->buildAElement( $target, $text, $attribs, true );
297 * @param LinkTarget $target
298 * @param string|HtmlArmor|null $text
299 * @param array $extraAttribs
300 * @param array $query
303 public function makeBrokenLink(
304 LinkTarget
$target, $text = null, array $extraAttribs = [], array $query = []
307 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, false );
308 if ( $ret !== null ) {
312 # We don't want to include fragments for broken links, because they
313 # generally make no sense.
314 if ( $target->hasFragment() ) {
315 $target = $target->createFragmentTarget( '' );
317 $target = $this->normalizeTarget( $target );
319 if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL
) {
320 $query['action'] = 'edit';
321 $query['redlink'] = '1';
324 $url = $this->getLinkURL( $target, $query );
325 $attribs = $this->noClasses ?
[] : [ 'class' => 'new' ];
326 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
327 if ( $prefixedText !== '' ) {
328 // This ends up in parser cache!
329 $attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
330 ->inContentLanguage()
336 ] +
$this->mergeAttribs( $attribs, $extraAttribs );
338 if ( $text === null ) {
339 $text = $this->getLinkText( $target );
342 return $this->buildAElement( $target, $text, $attribs, false );
346 * Builds the final <a> element
348 * @param LinkTarget $target
349 * @param string|HtmlArmor $text
350 * @param array $attribs
351 * @param bool $isKnown
352 * @return null|string
354 private function buildAElement( LinkTarget
$target, $text, array $attribs, $isKnown ) {
356 if ( !Hooks
::run( 'HtmlPageLinkRendererEnd',
357 [ $this, $target, $isKnown, &$text, &$attribs, &$ret ] )
362 $html = HtmlArmor
::getHtml( $text );
365 if ( Hooks
::isRegistered( 'LinkEnd' ) ) {
366 $dummy = new DummyLinker();
367 $title = Title
::newFromLinkTarget( $target );
368 $options = $this->getLegacyOptions( $isKnown );
369 if ( !Hooks
::run( 'LinkEnd',
370 [ $dummy, $title, $options, &$html, &$attribs, &$ret ] )
376 return Html
::rawElement( 'a', $attribs, $html );
380 * @param LinkTarget $target
381 * @return string non-escaped text
383 private function getLinkText( LinkTarget
$target ) {
384 $prefixedText = $this->titleFormatter
->getPrefixedText( $target );
385 // If the target is just a fragment, with no title, we return the fragment
386 // text. Otherwise, we return the title text itself.
387 if ( $prefixedText === '' && $target->hasFragment() ) {
388 return $target->getFragment();
391 return $prefixedText;
394 private function getLinkURL( LinkTarget
$target, array $query = [] ) {
395 // TODO: Use a LinkTargetResolver service instead of Title
396 $title = Title
::newFromLinkTarget( $target );
397 $proto = $this->expandUrls
!== false
400 if ( $this->forceArticlePath
) {
406 $url = $title->getLinkURL( $query, false, $proto );
408 if ( $this->forceArticlePath
&& $realQuery ) {
409 $url = wfAppendQuery( $url, $realQuery );
416 * Normalizes the provided target
418 * @todo move the code from Linker actually here
419 * @param LinkTarget $target
422 private function normalizeTarget( LinkTarget
$target ) {
423 return Linker
::normaliseSpecialPage( $target );
427 * Merges two sets of attributes
429 * @param array $defaults
430 * @param array $attribs
434 private function mergeAttribs( $defaults, $attribs ) {
438 # Merge the custom attribs with the default ones, and iterate
439 # over that, deleting all "false" attributes.
441 $merged = Sanitizer
::mergeAttributes( $defaults, $attribs );
442 foreach ( $merged as $key => $val ) {
443 # A false value suppresses the attribute
444 if ( $val !== false ) {